package im.zego;

import android.app.Application;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import im.zego.zim.ZIM;
import im.zego.zim.callback.*;
import im.zego.zim.entity.*;
import im.zego.zim.enums.*;
import im.zego.zim.internal.util.ZIMLogUtil;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONObject;

public class RNZimReactnativeSdkModule extends ReactContextBaseJavaModule {

    private static final String Prefix = "im.zego.zim.reactnative.";

    private final ReactApplicationContext reactContext;

    private HashMap<String, ZIM> engineMap;
    private HashMap<ZIM, String> engineMapForCallback;

    public RNZimReactnativeSdkModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
        this.engineMap = new HashMap<>();
        this.engineMapForCallback = new HashMap<>();
    }

    @Override
    public String getName() {
        return "ZIMNativeModule";
    }

    @Override
    public Map<String, Object> getConstants() {
        final Map<String, Object> constants = new HashMap<>();
        constants.put("prefix", Prefix);
        return constants;
    }

    // Used for passive call
    private void sendEvent(String eventName, WritableMap dataMap, ZIM zim) {
        WritableMap paramsMap = Arguments.createMap();
        paramsMap.putString("handle", this.engineMapForCallback.get(zim));
        paramsMap.putMap("data", dataMap);

        this.reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
            .emit(Prefix + eventName, paramsMap);
    }

    // Used for API active call
    private void sendAPIEvent(String eventName, WritableMap params) {
        this.reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
            .emit(Prefix + eventName, params);
    }

    @ReactMethod
    public void addListener(String eventName) {}

    @ReactMethod
    public void removeListeners(Integer count) {}

    @ReactMethod
    public void getVersion(final Promise promise) {
        promise.resolve(ZIM.getVersion());
    }

    @ReactMethod
    public void writeCustomLog(String customLog, String moduleName) {
        try {
            // Class<?> clz = Class.forName("im.zego.zim.internal.util.ZIMLogUtil");
            Class<ZIMLogUtil> clz = ZIMLogUtil.class;
            Method method = clz.getDeclaredMethod("writeCustomLog", String.class, String.class);
            method.setAccessible(true);
            method.invoke(null, customLog, moduleName);
            method.setAccessible(false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @ReactMethod
    public void setLogConfig(ReadableMap config) {
        ZIMLogConfig configObj = new ZIMLogConfig();
        if (config.hasKey("logPath")) {
            configObj.logPath = config.getString("logPath");
        }

        if (config.hasKey("logSize")) {
            configObj.logSize = Double.valueOf(config.getDouble("logSize")).longValue();
        }

        ZIM.setLogConfig(configObj);
    }

    @ReactMethod
    public void setCacheConfig(ReadableMap config) {
        ZIMCacheConfig configObj = new ZIMCacheConfig();
        if (config.hasKey("cachePath")) {
            configObj.cachePath = config.getString("cachePath");
        }

        ZIM.setCacheConfig(configObj);
    }

    @ReactMethod
    public void setAdvancedConfig(String key, String value) {
        ZIM.setAdvancedConfig(key, value);
    }

    @ReactMethod
    public void setGeofencingConfig(ReadableArray areaList, int type, final Promise promise) {
        ArrayList<Integer> areas = new ArrayList<>();
        for (Object key : areaList.toArrayList()) {
            areas.add(Double.valueOf((double) key).intValue());
        }
        promise.resolve(ZIM.setGeofencingConfig(areas, ZIMGeofencingType.getZIMGeofencingType(type)));
    }

    @ReactMethod
    public void createEngine(String handle, Double appID, String appSign) {
        ZIM oldZIM = ZIM.getInstance();
        if (oldZIM != null) {
            oldZIM.destroy();
        }

        ZIMAppConfig appConfig = new ZIMAppConfig();
        appConfig.appID = appID.longValue();
        appConfig.appSign = appSign;

        ZIM zim = ZIM.create(appConfig, (Application) this.reactContext.getApplicationContext());
        if (zim != null) {
            this.engineMap.put(handle, zim);
            this.engineMapForCallback.put(zim, handle);

            zim.setEventHandler(
                new ZIMEventHandler() {
                    @Override
                    public void onError(ZIM zim, ZIMError errorInfo) {
                        super.onError(zim, errorInfo);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("code", errorInfo.code.value());
                        dataMap.putString("message", errorInfo.message);

                        sendEvent("error", dataMap, zim);
                    }

                    @Override
                    public void onConnectionStateChanged(
                        ZIM zim,
                        ZIMConnectionState state,
                        ZIMConnectionEvent event,
                        JSONObject extendedData
                    ) {
                        super.onConnectionStateChanged(zim, state, event, extendedData);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("state", state.value());
                        dataMap.putInt("event", event.value());
                        dataMap.putString("extendedData", extendedData.toString());

                        sendEvent("connectionStateChanged", dataMap, zim);
                    }

                    @Override
                    public void onTokenWillExpire(ZIM zim, int second) {
                        super.onTokenWillExpire(zim, second);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("second", second);

                        sendEvent("tokenWillExpire", dataMap, zim);
                    }

                    @Override
                    public void onUserInfoUpdated(ZIM zim, ZIMUserFullInfo info) {
                        super.onUserInfoUpdated(zim, info);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putMap("info", RNZIMConverter.convertUserFullInfo(info));

                        sendEvent("userInfoUpdated", dataMap, zim);
                    }

                    @Override
                    public void onUserStatusUpdated(ZIM zim, ArrayList<ZIMUserStatus> userStatusList) {
                        super.onUserStatusUpdated(zim, userStatusList);

                        WritableMap dataMap = Arguments.createMap();

                        WritableArray userStatusArray = Arguments.createArray();
                        for (ZIMUserStatus userStatus : userStatusList) {
                            userStatusArray.pushMap(RNZIMConverter.convertUserStatus(userStatus));
                        }
                        dataMap.putArray("userStatusList", userStatusArray);

                        sendEvent("userStatusUpdated", dataMap, zim);
                    }

                    @Override
                    public void onUserRuleUpdated(ZIM zim, ZIMUserRule info) {
                        super.onUserRuleUpdated(zim, info);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putMap("userRule", RNZIMConverter.convertUserRule(info.offlinePushRule));

                        sendEvent("userRuleUpdated", dataMap, zim);
                    }

                    @Override
                    public void onConversationsAllDeleted(ZIM zim, ZIMConversationsAllDeletedInfo info) {
                        super.onConversationsAllDeleted(zim, info);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("count", info.count);

                        sendEvent("conversationsAllDeleted", dataMap, zim);
                    }

                    @Override
                    public void onConversationChanged(
                        ZIM zim,
                        ArrayList<ZIMConversationChangeInfo> conversationChangeInfoList
                    ) {
                        super.onConversationChanged(zim, conversationChangeInfoList);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putArray(
                            "infoList",
                            RNZIMConverter.convertConversationChangeInfo(conversationChangeInfoList)
                        );

                        sendEvent("conversationChanged", dataMap, zim);
                    }

                    @Override
                    public void onConversationTotalUnreadMessageCountUpdated(ZIM zim, int totalUnreadMessageCount) {
                        super.onConversationTotalUnreadMessageCountUpdated(zim, totalUnreadMessageCount);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("totalUnreadMessageCount", totalUnreadMessageCount);

                        sendEvent("conversationTotalUnreadMessageCountUpdated", dataMap, zim);
                    }

                    @Override
                    public void onReceivePeerMessage(ZIM zim, ArrayList<ZIMMessage> messageList, String fromUserID) {
                        super.onReceivePeerMessage(zim, messageList, fromUserID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("fromConversationID", fromUserID);
                        dataMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        sendEvent("receivePeerMessage", dataMap, zim);
                    }

                    @Override
                    public void onReceiveRoomMessage(ZIM zim, ArrayList<ZIMMessage> messageList, String fromRoomID) {
                        super.onReceiveRoomMessage(zim, messageList, fromRoomID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("fromConversationID", fromRoomID);
                        dataMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        sendEvent("receiveRoomMessage", dataMap, zim);
                    }

                    @Override
                    public void onReceiveGroupMessage(ZIM zim, ArrayList<ZIMMessage> messageList, String fromGroupID) {
                        super.onReceiveGroupMessage(zim, messageList, fromGroupID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("fromConversationID", fromGroupID);
                        dataMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        sendEvent("receiveGroupMessage", dataMap, zim);
                    }

                    @Override
                    public void onPeerMessageReceived(
                        ZIM zim,
                        ArrayList<ZIMMessage> messageList,
                        ZIMMessageReceivedInfo info,
                        String fromConvID
                    ) {
                        super.onPeerMessageReceived(zim, messageList, info, fromConvID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("fromConversationID", fromConvID);
                        dataMap.putMap("info", RNZIMConverter.convertMessageReceivedInfo(info));
                        dataMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        sendEvent("peerMessageReceived", dataMap, zim);
                    }

                    @Override
                    public void onRoomMessageReceived(
                        ZIM zim,
                        ArrayList<ZIMMessage> messageList,
                        ZIMMessageReceivedInfo info,
                        String fromConvID
                    ) {
                        super.onRoomMessageReceived(zim, messageList, info, fromConvID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("fromConversationID", fromConvID);
                        dataMap.putMap("info", RNZIMConverter.convertMessageReceivedInfo(info));
                        dataMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        sendEvent("roomMessageReceived", dataMap, zim);
                    }

                    @Override
                    public void onGroupMessageReceived(
                        ZIM zim,
                        ArrayList<ZIMMessage> messageList,
                        ZIMMessageReceivedInfo info,
                        String fromConvID
                    ) {
                        super.onGroupMessageReceived(zim, messageList, info, fromConvID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("fromConversationID", fromConvID);
                        dataMap.putMap("info", RNZIMConverter.convertMessageReceivedInfo(info));
                        dataMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        sendEvent("groupMessageReceived", dataMap, zim);
                    }

                    @Override
                    public void onBroadcastMessageReceived(ZIM zim, ZIMMessage message) {
                        super.onBroadcastMessageReceived(zim, message);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putMap("message", RNZIMConverter.convertMessage(message));

                        sendEvent("broadcastMessageReceived", dataMap, zim);
                    }

                    @Override
                    public void onConversationMessageReceiptChanged(ZIM zim, ArrayList<ZIMMessageReceiptInfo> infos) {
                        super.onConversationMessageReceiptChanged(zim, infos);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putArray("infos", RNZIMConverter.convertMessageReceiptInfoList(infos));

                        sendEvent("conversationMessageReceiptChanged", dataMap, zim);
                    }

                    @Override
                    public void onMessageReceiptChanged(ZIM zim, ArrayList<ZIMMessageReceiptInfo> infos) {
                        super.onMessageReceiptChanged(zim, infos);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putArray("infos", RNZIMConverter.convertMessageReceiptInfoList(infos));

                        sendEvent("messageReceiptChanged", dataMap, zim);
                    }

                    @Override
                    public void onMessageRevokeReceived(ZIM zim, ArrayList<ZIMRevokeMessage> messageList) {
                        super.onMessageRevokeReceived(zim, messageList);

                        WritableMap dataMap = Arguments.createMap();
                        WritableArray messageArray = Arguments.createArray();
                        for (ZIMMessage message : messageList) {
                            messageArray.pushMap(RNZIMConverter.convertMessage(message));
                        }
                        dataMap.putArray("messageList", messageArray);

                        sendEvent("messageRevokeReceived", dataMap, zim);
                    }

                    @Override
                    public void onMessageReactionsChanged(ZIM zim, ArrayList<ZIMMessageReaction> reactions) {
                        super.onMessageReactionsChanged(zim, reactions);

                        WritableMap dataMap = Arguments.createMap();
                        WritableArray infosArray = Arguments.createArray();
                        for (ZIMMessageReaction info : reactions) {
                            infosArray.pushMap(RNZIMConverter.convertMessageReaction(info));
                        }
                        dataMap.putArray("reactions", infosArray);

                        sendEvent("messageReactionsChanged", dataMap, zim);
                    }

                    @Override
                    public void onMessageRepliedCountChanged(ZIM zim, ArrayList<ZIMMessageRootRepliedCountInfo> infos) {
                        super.onMessageRepliedCountChanged(zim, infos);

                        WritableArray infosArray = Arguments.createArray();
                        for (ZIMMessageRootRepliedCountInfo info : infos) {
                            WritableMap infoMap = Arguments.createMap();
                            infoMap.putString("conversationID", info.conversationID);
                            infoMap.putInt("conversationType", info.conversationType.value());
                            infoMap.putInt("count", info.count);
                            infoMap.putString("messageID", String.valueOf(info.messageID));
                            infosArray.pushMap(infoMap);
                        }

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putArray("infos", infosArray);

                        sendEvent("messageRepliedCountChanged", dataMap, zim);
                    }

                    @Override
                    public void onMessageRepliedInfoChanged(ZIM zim, ArrayList<ZIMMessage> messageList) {
                        super.onMessageRepliedInfoChanged(zim, messageList);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        sendEvent("messageRepliedInfoChanged", dataMap, zim);
                    }

                    @Override
                    public void onMessageEdited(ZIM zim, ArrayList<ZIMMessage> messageList) {
                        super.onMessageEdited(zim, messageList);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        sendEvent("messageEdited", dataMap, zim);
                    }

                    @Override
                    public void onMessageDeleted(ZIM zim, ZIMMessageDeletedInfo deletedInfo) {
                        super.onMessageDeleted(zim, deletedInfo);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("conversationID", deletedInfo.conversationID);
                        dataMap.putInt("conversationType", deletedInfo.conversationType.value());
                        dataMap.putBoolean(
                            "isDeleteConversationAllMessage",
                            deletedInfo.isDeleteConversationAllMessage
                        );
                        dataMap.putInt("messageDeleteType", deletedInfo.messageDeleteType.value());

                        WritableArray messageArray = Arguments.createArray();
                        for (ZIMMessage message : deletedInfo.messageList) {
                            messageArray.pushMap(RNZIMConverter.convertMessage(message));
                        }
                        dataMap.putArray("messageList", messageArray);

                        sendEvent("messageDeleted", dataMap, zim);
                    }

                    @Override
                    public void onMessageSentStatusChanged(
                        ZIM zim,
                        ArrayList<ZIMMessageSentStatusChangeInfo> messageSentStatusChangeInfoList
                    ) {
                        super.onMessageSentStatusChanged(zim, messageSentStatusChangeInfoList);

                        WritableMap dataMap = Arguments.createMap();
                        WritableArray infosArray = Arguments.createArray();
                        for (ZIMMessageSentStatusChangeInfo info : messageSentStatusChangeInfoList) {
                            infosArray.pushMap(RNZIMConverter.convertMessageSentStatusChangeInfo(info));
                        }
                        dataMap.putArray("infos", infosArray);

                        sendEvent("messageSentStatusChanged", dataMap, zim);
                    }

                    @Override
                    public void onRoomMemberJoined(ZIM zim, ArrayList<ZIMUserInfo> memberList, String roomID) {
                        super.onRoomMemberJoined(zim, memberList, roomID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("roomID", roomID);
                        dataMap.putArray("memberList", RNZIMConverter.convertUserInfoList(memberList));

                        sendEvent("roomMemberJoined", dataMap, zim);
                    }

                    @Override
                    public void onRoomMemberLeft(ZIM zim, ArrayList<ZIMUserInfo> memberList, String roomID) {
                        super.onRoomMemberLeft(zim, memberList, roomID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("roomID", roomID);
                        dataMap.putArray("memberList", RNZIMConverter.convertUserInfoList(memberList));

                        sendEvent("roomMemberLeft", dataMap, zim);
                    }

                    @Override
                    public void onRoomStateChanged(
                        ZIM zim,
                        ZIMRoomState state,
                        ZIMRoomEvent event,
                        JSONObject extendedData,
                        String roomID
                    ) {
                        super.onRoomStateChanged(zim, state, event, extendedData, roomID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("state", state.value());
                        dataMap.putInt("event", event.value());
                        dataMap.putString("extendedData", extendedData.toString());
                        dataMap.putString("roomID", roomID);

                        sendEvent("roomStateChanged", dataMap, zim);
                    }

                    @Override
                    public void onRoomAttributesUpdated(ZIM zim, ZIMRoomAttributesUpdateInfo info, String roomID) {
                        super.onRoomAttributesUpdated(zim, info, roomID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("roomID", roomID);

                        WritableArray infoArray = Arguments.createArray();
                        WritableMap infoMap = Arguments.createMap();
                        WritableMap roomAttrMap = Arguments.createMap();
                        for (String key : info.roomAttributes.keySet()) {
                            roomAttrMap.putString(key, info.roomAttributes.get(key));
                        }
                        infoMap.putInt("action", info.action.value());
                        infoMap.putMap("roomAttributes", roomAttrMap);
                        infoArray.pushMap(infoMap);

                        dataMap.putArray("infos", infoArray);

                        sendEvent("roomAttributesUpdated", dataMap, zim);
                    }

                    @Override
                    public void onRoomAttributesBatchUpdated(
                        ZIM zim,
                        ArrayList<ZIMRoomAttributesUpdateInfo> infos,
                        String roomID
                    ) {
                        super.onRoomAttributesBatchUpdated(zim, infos, roomID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("roomID", roomID);

                        WritableArray infoArray = Arguments.createArray();
                        for (ZIMRoomAttributesUpdateInfo info : infos) {
                            WritableMap infoMap = Arguments.createMap();
                            WritableMap roomAttrMap = Arguments.createMap();
                            for (String key : info.roomAttributes.keySet()) {
                                roomAttrMap.putString(key, info.roomAttributes.get(key));
                            }
                            infoMap.putInt("action", info.action.value());
                            infoMap.putMap("roomAttributes", roomAttrMap);
                            infoArray.pushMap(infoMap);
                        }

                        dataMap.putArray("infos", infoArray);

                        sendEvent("roomAttributesBatchUpdated", dataMap, zim);
                    }

                    @Override
                    public void onRoomMemberAttributesUpdated(
                        ZIM zim,
                        ArrayList<ZIMRoomMemberAttributesUpdateInfo> infos,
                        ZIMRoomOperatedInfo operatedInfo,
                        String roomID
                    ) {
                        super.onRoomMemberAttributesUpdated(zim, infos, operatedInfo, roomID);

                        WritableMap dataMap = Arguments.createMap();
                        ReadableArray infoArray = RNZIMConverter.convertRoomMemberAttributesUpdateInfoList(infos);
                        dataMap.putArray("infos", infoArray);
                        dataMap.putMap("operatedInfo", RNZIMConverter.convertRoomOperatedInfo(operatedInfo));
                        dataMap.putString("roomID", roomID);

                        sendEvent("roomMemberAttributesUpdated", dataMap, zim);
                    }

                    @Override
                    public void onGroupStateChanged(
                        ZIM zim,
                        ZIMGroupState state,
                        ZIMGroupEvent event,
                        ZIMGroupOperatedInfo operatedInfo,
                        ZIMGroupFullInfo groupInfo
                    ) {
                        super.onGroupStateChanged(zim, state, event, operatedInfo, groupInfo);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("state", state.value());
                        dataMap.putInt("event", event.value());
                        dataMap.putMap("operatedInfo", RNZIMConverter.convertGroupOperatedInfo(operatedInfo));
                        dataMap.putMap("groupInfo", RNZIMConverter.convertGroupFullInfo(groupInfo));

                        sendEvent("groupStateChanged", dataMap, zim);
                    }

                    @Override
                    public void onGroupNameUpdated(
                        ZIM zim,
                        String groupName,
                        ZIMGroupOperatedInfo operatedInfo,
                        String groupID
                    ) {
                        super.onGroupNameUpdated(zim, groupName, operatedInfo, groupID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("groupID", groupID);
                        dataMap.putString("groupName", groupName);
                        dataMap.putMap("operatedInfo", RNZIMConverter.convertGroupOperatedInfo(operatedInfo));

                        sendEvent("groupNameUpdated", dataMap, zim);
                    }

                    @Override
                    public void onGroupAvatarUrlUpdated(
                        ZIM zim,
                        String groupAvatarUrl,
                        ZIMGroupOperatedInfo operatedInfo,
                        String groupID
                    ) {
                        super.onGroupAvatarUrlUpdated(zim, groupAvatarUrl, operatedInfo, groupID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("groupID", groupID);
                        dataMap.putString("groupAvatarUrl", groupAvatarUrl);
                        dataMap.putMap("operatedInfo", RNZIMConverter.convertGroupOperatedInfo(operatedInfo));

                        sendEvent("groupAvatarUrlUpdated", dataMap, zim);
                    }

                    @Override
                    public void onGroupAliasUpdated(
                        ZIM zim,
                        String groupAlias,
                        String operatedUserID,
                        String groupID
                    ) {
                        super.onGroupAliasUpdated(zim, groupAlias, operatedUserID, groupID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("groupID", groupID);
                        dataMap.putString("groupAlias", groupAlias);
                        dataMap.putString("operatedUserID", operatedUserID);

                        sendEvent("groupAliasUpdated", dataMap, zim);
                    }

                    @Override
                    public void onGroupNoticeUpdated(
                        ZIM zim,
                        String groupNotice,
                        ZIMGroupOperatedInfo operatedInfo,
                        String groupID
                    ) {
                        super.onGroupNoticeUpdated(zim, groupNotice, operatedInfo, groupID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("groupID", groupID);
                        dataMap.putString("groupNotice", groupNotice);
                        dataMap.putMap("operatedInfo", RNZIMConverter.convertGroupOperatedInfo(operatedInfo));

                        sendEvent("groupNoticeUpdated", dataMap, zim);
                    }

                    @Override
                    public void onGroupAttributesUpdated(
                        ZIM zim,
                        ArrayList<ZIMGroupAttributesUpdateInfo> infos,
                        ZIMGroupOperatedInfo operatedInfo,
                        String groupID
                    ) {
                        super.onGroupAttributesUpdated(zim, infos, operatedInfo, groupID);

                        WritableMap dataMap = Arguments.createMap();
                        WritableArray infoArray = Arguments.createArray();
                        for (ZIMGroupAttributesUpdateInfo info : infos) {
                            WritableMap infoMap = Arguments.createMap();
                            infoMap.putInt("action", info.action.value());

                            WritableMap groupAttrMap = Arguments.createMap();
                            for (String key : info.groupAttributes.keySet()) {
                                groupAttrMap.putString(key, info.groupAttributes.get(key));
                            }
                            infoMap.putMap("groupAttributes", groupAttrMap);

                            infoArray.pushMap(infoMap);
                        }
                        dataMap.putArray("infoList", infoArray);
                        dataMap.putMap("operatedInfo", RNZIMConverter.convertGroupOperatedInfo(operatedInfo));
                        dataMap.putString("groupID", groupID);

                        sendEvent("groupAttributesUpdated", dataMap, zim);
                    }

                    @Override
                    public void onGroupMutedInfoUpdated(
                        ZIM zim,
                        ZIMGroupMuteInfo info,
                        ZIMGroupOperatedInfo operatedInfo,
                        String groupID
                    ) {
                        super.onGroupMutedInfoUpdated(zim, info, operatedInfo, groupID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putMap("mutedInfo", RNZIMConverter.convertGroupMuteInfo(info));
                        dataMap.putMap("operatedInfo", RNZIMConverter.convertGroupOperatedInfo(operatedInfo));
                        dataMap.putString("groupID", groupID);

                        sendEvent("groupMutedInfoUpdated", dataMap, zim);
                    }

                    @Override
                    public void onGroupVerifyInfoUpdated(
                        ZIM zim,
                        ZIMGroupVerifyInfo info,
                        ZIMGroupOperatedInfo operatedInfo,
                        String groupID
                    ) {
                        super.onGroupVerifyInfoUpdated(zim, info, operatedInfo, groupID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putMap("verifyInfo", RNZIMConverter.convertGroupVerifyInfo(info));
                        dataMap.putMap("operatedInfo", RNZIMConverter.convertGroupOperatedInfo(operatedInfo));
                        dataMap.putString("groupID", groupID);

                        sendEvent("groupVerifyInfoUpdated", dataMap, zim);
                    }

                    @Override
                    public void onGroupApplicationListChanged(
                        ZIM zim,
                        ArrayList<ZIMGroupApplicationInfo> infos,
                        ZIMGroupApplicationListChangeAction action
                    ) {
                        super.onGroupApplicationListChanged(zim, infos, action);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("action", action.value());
                        dataMap.putArray("applicationList", RNZIMConverter.convertGroupApplicationList(infos));

                        sendEvent("groupApplicationListChanged", dataMap, zim);
                    }

                    @Override
                    public void onGroupApplicationUpdated(ZIM zim, ArrayList<ZIMGroupApplicationInfo> infos) {
                        super.onGroupApplicationUpdated(zim, infos);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putArray("applicationList", RNZIMConverter.convertGroupApplicationList(infos));

                        sendEvent("groupApplicationUpdated", dataMap, zim);
                    }

                    @Override
                    public void onGroupMemberStateChanged(
                        ZIM zim,
                        ZIMGroupMemberState state,
                        ZIMGroupMemberEvent event,
                        ArrayList<ZIMGroupMemberInfo> userList,
                        ZIMGroupOperatedInfo operatedInfo,
                        String groupID
                    ) {
                        super.onGroupMemberStateChanged(zim, state, event, userList, operatedInfo, groupID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("state", state.value());
                        dataMap.putInt("event", event.value());
                        dataMap.putArray("userList", RNZIMConverter.convertGroupMemberInfoList(userList));
                        dataMap.putMap("operatedInfo", RNZIMConverter.convertGroupOperatedInfo(operatedInfo));
                        dataMap.putString("groupID", groupID);

                        sendEvent("groupMemberStateChanged", dataMap, zim);
                    }

                    @Override
                    public void onGroupMemberInfoUpdated(
                        ZIM zim,
                        ArrayList<ZIMGroupMemberInfo> userList,
                        ZIMGroupOperatedInfo operatedInfo,
                        String groupID
                    ) {
                        super.onGroupMemberInfoUpdated(zim, userList, operatedInfo, groupID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putArray("userList", RNZIMConverter.convertGroupMemberInfoList(userList));
                        dataMap.putMap("operatedInfo", RNZIMConverter.convertGroupOperatedInfo(operatedInfo));
                        dataMap.putString("groupID", groupID);

                        sendEvent("groupMemberInfoUpdated", dataMap, zim);
                    }

                    @Override
                    public void onCallInvitationCreated(ZIM zim, ZIMCallInvitationCreatedInfo info, String callID) {
                        super.onCallInvitationCreated(zim, info, callID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("callID", callID);
                        dataMap.putInt("timeout", info.timeout);
                        dataMap.putString("extendedData", info.extendedData);
                        dataMap.putInt("mode", info.mode.value());
                        dataMap.putString("caller", info.caller);
                        dataMap.putDouble("createTime", info.createTime);
                        dataMap.putArray("callUserList", RNZIMConverter.convertCallUserList(info.callUserList));

                        sendEvent("callInvitationCreated", dataMap, zim);
                    }

                    @Override
                    public void onCallInvitationReceived(ZIM zim, ZIMCallInvitationReceivedInfo info, String callID) {
                        super.onCallInvitationReceived(zim, info, callID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("callID", callID);
                        dataMap.putString("inviter", info.inviter);
                        dataMap.putInt("timeout", info.timeout);
                        dataMap.putString("extendedData", info.extendedData);
                        dataMap.putInt("mode", info.mode.value());
                        dataMap.putString("caller", info.caller);
                        dataMap.putDouble("createTime", info.createTime);
                        dataMap.putArray("callUserList", RNZIMConverter.convertCallUserList(info.callUserList));

                        sendEvent("callInvitationReceived", dataMap, zim);
                    }

                    @Override
                    public void onCallInvitationCancelled(ZIM zim, ZIMCallInvitationCancelledInfo info, String callID) {
                        super.onCallInvitationCancelled(zim, info, callID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("callID", callID);
                        dataMap.putString("inviter", info.inviter);
                        dataMap.putString("extendedData", info.extendedData);
                        dataMap.putInt("mode", info.mode.value());

                        sendEvent("callInvitationCancelled", dataMap, zim);
                    }

                    @Override
                    public void onCallInvitationTimeout(ZIM zim, ZIMCallInvitationTimeoutInfo info, String callID) {
                        super.onCallInvitationTimeout(zim, callID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("callID", callID);
                        dataMap.putInt("mode", info.mode.value());

                        sendEvent("callInvitationTimeout", dataMap, zim);
                    }

                    @Override
                    public void onCallInvitationEnded(ZIM zim, ZIMCallInvitationEndedInfo info, String callID) {
                        super.onCallInvitationEnded(zim, info, callID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("callID", callID);
                        dataMap.putInt("mode", info.mode.value());
                        dataMap.putString("caller", info.caller);
                        dataMap.putString("operatedUserID", info.operatedUserID);
                        dataMap.putString("extendedData", info.extendedData);
                        dataMap.putDouble("endTime", info.endTime);

                        sendEvent("callInvitationEnded", dataMap, zim);
                    }

                    @Override
                    public void onCallUserStateChanged(ZIM zim, ZIMCallUserStateChangeInfo info, String callID) {
                        super.onCallUserStateChanged(zim, info, callID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("callID", callID);
                        dataMap.putArray("callUserList", RNZIMConverter.convertCallUserList(info.callUserList));

                        sendEvent("callUserStateChanged", dataMap, zim);
                    }

                    @Override
                    public void onCallInvitationAccepted(ZIM zim, ZIMCallInvitationAcceptedInfo info, String callID) {
                        super.onCallInvitationAccepted(zim, info, callID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("callID", callID);
                        dataMap.putString("invitee", info.invitee);
                        dataMap.putString("extendedData", info.extendedData);

                        sendEvent("callInvitationAccepted", dataMap, zim);
                    }

                    @Override
                    public void onCallInvitationRejected(ZIM zim, ZIMCallInvitationRejectedInfo info, String callID) {
                        super.onCallInvitationRejected(zim, info, callID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("callID", callID);
                        dataMap.putString("invitee", info.invitee);
                        dataMap.putString("extendedData", info.extendedData);

                        sendEvent("callInvitationRejected", dataMap, zim);
                    }

                    @Override
                    public void onCallInviteesAnsweredTimeout(ZIM zim, ArrayList<String> invitees, String callID) {
                        super.onCallInviteesAnsweredTimeout(zim, invitees, callID);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putString("callID", callID);
                        WritableArray inviteesArray = Arguments.createArray();
                        for (String invitee : invitees) {
                            inviteesArray.pushString(invitee);
                        }
                        dataMap.putArray("invitees", inviteesArray);

                        sendEvent("callInviteesAnsweredTimeout", dataMap, zim);
                    }

                    @Override
                    public void onBlacklistChanged(
                        ZIM zim,
                        ArrayList<ZIMUserInfo> infos,
                        ZIMBlacklistChangeAction action
                    ) {
                        super.onBlacklistChanged(zim, infos, action);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("action", action.value());
                        dataMap.putArray("userList", RNZIMConverter.convertUserInfoList(infos));

                        sendEvent("blacklistChanged", dataMap, zim);
                    }

                    @Override
                    public void onFriendListChanged(
                        ZIM zim,
                        ArrayList<ZIMFriendInfo> infos,
                        ZIMFriendListChangeAction action
                    ) {
                        super.onFriendListChanged(zim, infos, action);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("action", action.value());
                        dataMap.putArray("friendList", RNZIMConverter.convertFriendInfoList(infos));

                        sendEvent("friendListChanged", dataMap, zim);
                    }

                    @Override
                    public void onFriendInfoUpdated(ZIM zim, ArrayList<ZIMFriendInfo> infos) {
                        super.onFriendInfoUpdated(zim, infos);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putArray("userList", RNZIMConverter.convertFriendInfoList(infos));

                        sendEvent("friendInfoUpdated", dataMap, zim);
                    }

                    @Override
                    public void onFriendApplicationListChanged(
                        ZIM zim,
                        ArrayList<ZIMFriendApplicationInfo> infos,
                        ZIMFriendApplicationListChangeAction action
                    ) {
                        super.onFriendApplicationListChanged(zim, infos, action);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putInt("action", action.value());
                        dataMap.putArray("applicationList", RNZIMConverter.convertFriendApplicationList(infos));

                        sendEvent("friendApplicationListChanged", dataMap, zim);
                    }

                    @Override
                    public void onFriendApplicationUpdated(ZIM zim, ArrayList<ZIMFriendApplicationInfo> infos) {
                        super.onFriendApplicationUpdated(zim, infos);

                        WritableMap dataMap = Arguments.createMap();
                        dataMap.putArray("applicationList", RNZIMConverter.convertFriendApplicationList(infos));

                        sendEvent("friendApplicationUpdated", dataMap, zim);
                    }
                }
            );
        }
    }

    @ReactMethod
    public void destroyEngine(String handle) {
        ZIM zim = this.engineMap.get(handle);
        if (zim != null) {
            zim.destroy();

            this.engineMap.remove(handle);
            this.engineMapForCallback.remove(zim);
        }
    }

    @ReactMethod
    public void uploadLog(String handle, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.uploadLog(
            new ZIMLogUploadedCallback() {
                @Override
                public void onLogUploaded(ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        promise.resolve(null);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void login(String handle, String userID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMLoginConfig configObj = new ZIMLoginConfig();
        configObj.userName = config.hasKey("userName") ? config.getString("userName") : "";
        configObj.customStatus = config.hasKey("customStatus") ? config.getString("customStatus") : "";
        configObj.token = config.hasKey("token") ? config.getString("token") : "";
        configObj.isOfflineLogin = config.hasKey("isOfflineLogin") ? config.getBoolean("isOfflineLogin") : false;

        zim.login(
            userID,
            configObj,
            new ZIMLoggedInCallback() {
                @Override
                public void onLoggedIn(ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        promise.resolve(null);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void logout(String handle, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.logout();

        promise.resolve(null);
    }

    @ReactMethod
    public void renewToken(String handle, String token, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.renewToken(
            token,
            new ZIMTokenRenewedCallback() {
                @Override
                public void onTokenRenewed(String token, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("token", token);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryUsersInfo(String handle, ReadableArray userIDs, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userList = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userList.add((String) userID);
        }

        ZIMUsersInfoQueryConfig configObj = new ZIMUsersInfoQueryConfig();
        if (config != null) {
            configObj.isQueryFromServer =
                config.hasKey("isQueryFromServer") ? config.getBoolean("isQueryFromServer") : false;
        }

        zim.queryUsersInfo(
            userList,
            configObj,
            new ZIMUsersInfoQueriedCallback() {
                @Override
                public void onUsersInfoQueried(
                    ArrayList<ZIMUserFullInfo> userList,
                    ArrayList<ZIMErrorUserInfo> errorUserList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        WritableArray userInfoArray = Arguments.createArray();
                        for (ZIMUserFullInfo userInfo : userList) {
                            userInfoArray.pushMap(RNZIMConverter.convertUserFullInfo(userInfo));
                        }

                        returnMap.putArray("userList", userInfoArray);
                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateUserName(String handle, String userName, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateUserName(
            userName,
            new ZIMUserNameUpdatedCallback() {
                @Override
                public void onUserNameUpdated(String userName, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("userName", userName);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateUserAvatarUrl(String handle, String userAvatarUrl, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateUserAvatarUrl(
            userAvatarUrl,
            new ZIMUserAvatarUrlUpdatedCallback() {
                @Override
                public void onUserAvatarUrlUpdated(String userAvatarUrl, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("userAvatarUrl", userAvatarUrl);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateUserExtendedData(String handle, String userExtendedData, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateUserExtendedData(
            userExtendedData,
            new ZIMUserExtendedDataUpdatedCallback() {
                @Override
                public void onUserExtendedDataUpdated(String extendedData, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("extendedData", extendedData);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateUserCustomStatus(String handle, String customStatus, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateUserCustomStatus(
            customStatus,
            new ZIMUserCustomStatusUpdatedCallback() {
                @Override
                public void onUserCustomStatusUpdated(String customStatus, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("customStatus", customStatus);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateUserOfflinePushRule(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMUserOfflinePushRule configObj = new ZIMUserOfflinePushRule();
        if (config != null) {
            if (config.hasKey("onlinePlatforms")) {
                ArrayList<Integer> onlinePlatforms = new ArrayList<>();
                for (Object key : config.getArray("onlinePlatforms").toArrayList()) {
                    onlinePlatforms.add(Double.valueOf((double) key).intValue());
                }
                configObj.onlinePlatforms = onlinePlatforms;
            }
            if (config.hasKey("notToReceiveOfflinePushPlatforms")) {
                ArrayList<Integer> offlinePushPlatforms = new ArrayList<>();
                for (Object key : config.getArray("notToReceiveOfflinePushPlatforms").toArrayList()) {
                    offlinePushPlatforms.add(Double.valueOf((double) key).intValue());
                }
                configObj.notToReceiveOfflinePushPlatforms = offlinePushPlatforms;
            }
        }

        zim.updateUserOfflinePushRule(
            configObj,
            new ZIMUserOfflinePushRuleUpdatedCallback() {
                @Override
                public void onUserOfflinePushRuleInfoUpdated(
                    ZIMUserOfflinePushRule offlinePushRule,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        promise.resolve(RNZIMConverter.convertUserRule(offlinePushRule));
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void querySelfUserInfo(String handle, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.querySelfUserInfo(
            new ZIMSelfUserInfoQueriedCallback() {
                @Override
                public void onSelfUserInfoQueried(ZIMSelfUserInfo info, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        WritableMap infoMap = Arguments.createMap();
                        infoMap.putMap("userRule", RNZIMConverter.convertUserRule(info.userRule.offlinePushRule));
                        infoMap.putMap("userStatus", RNZIMConverter.convertUserStatus(info.userStatus));
                        infoMap.putMap("userFullInfo", RNZIMConverter.convertUserFullInfo(info.userFullInfo));
                        returnMap.putMap("selfUserInfo", infoMap);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryLocalFileCache(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMFileCacheQueryConfig configObj = new ZIMFileCacheQueryConfig();
        configObj.endTime = config.hasKey("endTime") ? Double.valueOf(config.getDouble("endTime")).longValue() : 0;

        zim.queryLocalFileCache(
            configObj,
            new ZIMFileCacheQueriedCallback() {
                @Override
                public void onFileCacheQueried(ZIMFileCacheInfo info, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putDouble("totalFileSize", info.totalFileSize);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void clearLocalFileCache(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMFileCacheClearConfig configObj = new ZIMFileCacheClearConfig();
        configObj.endTime = config.hasKey("endTime") ? Double.valueOf(config.getDouble("endTime")).longValue() : 0;

        zim.clearLocalFileCache(
            configObj,
            new ZIMFileCacheClearedCallback() {
                @Override
                public void onFileCacheCleared(ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        promise.resolve(null);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void subscribeUsersStatus(
        String handle,
        boolean isSub,
        ReadableArray userIDs,
        int duration,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userList = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userList.add((String) userID);
        }

        if (isSub) {
            ZIMUserStatusSubscribeConfig configObj = new ZIMUserStatusSubscribeConfig();
            configObj.subscriptionDuration = duration;

            zim.subscribeUsersStatus(
                userList,
                configObj,
                new ZIMUsersStatusSubscribedCallback() {
                    @Override
                    public void onUsersStatusSubscribed(ArrayList<ZIMErrorUserInfo> errorUserList, ZIMError errorInfo) {
                        if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                            WritableMap returnMap = Arguments.createMap();
                            returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));
                            promise.resolve(returnMap);
                        } else {
                            promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                        }
                    }
                }
            );
        } else {
            zim.unsubscribeUsersStatus(
                userList,
                new ZIMUsersStatusUnsubscribedCallback() {
                    @Override
                    public void onUsersStatusUnsubscribed(
                        ArrayList<ZIMErrorUserInfo> errorUserList,
                        ZIMError errorInfo
                    ) {
                        if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                            WritableMap returnMap = Arguments.createMap();
                            returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));
                            promise.resolve(returnMap);
                        } else {
                            promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                        }
                    }
                }
            );
        }
    }

    @ReactMethod
    public void queryUsersStatus(String handle, ReadableArray userIDs, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userList = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userList.add((String) userID);
        }

        zim.queryUsersStatus(
            userList,
            new ZIMUsersStatusQueriedCallback() {
                @Override
                public void onUsersStatusQueried(
                    ArrayList<ZIMUserStatus> userStatusList,
                    ArrayList<ZIMErrorUserInfo> errorUserList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        WritableArray userStatusArray = Arguments.createArray();
                        for (ZIMUserStatus userStatus : userStatusList) {
                            userStatusArray.pushMap(RNZIMConverter.convertUserStatus(userStatus));
                        }

                        returnMap.putArray("userStatusList", userStatusArray);
                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void querySubscribedUsersStatus(String handle, ReadableArray userIDs, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userList = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userList.add((String) userID);
        }

        ZIMSubscribedUserStatusQueryConfig configObj = new ZIMSubscribedUserStatusQueryConfig();
        configObj.userIDs = userList;

        zim.querySubscribedUserStatusList(
            configObj,
            new ZIMSubscribedUserStatusListQueriedCallback() {
                @Override
                public void onSubscribedUserStatusListQueried(
                    ArrayList<ZIMUserStatusSubscription> userStatusSubscriptionList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        WritableArray userStatusArray = Arguments.createArray();
                        for (ZIMUserStatusSubscription info : userStatusSubscriptionList) {
                            WritableMap infoMap = Arguments.createMap();
                            infoMap.putMap("userStatus", RNZIMConverter.convertUserStatus(info.userStatus));
                            infoMap.putDouble("subscribeExpiredTime", info.subscribeExpiredTime);

                            userStatusArray.pushMap(infoMap);
                        }

                        returnMap.putArray("userStatusSubscriptionList", userStatusArray);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryConversation(String handle, String conversationID, int conversationType, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.queryConversation(
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            new ZIMConversationQueriedCallback() {
                @Override
                public void onConversationQueried(ZIMConversation conversation, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("conversation", RNZIMConverter.convertConversation(conversation));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryConversationList(String handle, ReadableMap config, ReadableMap option, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMConversationQueryConfig configObj = new ZIMConversationQueryConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextConversation =
                config.hasKey("nextConversation")
                    ? RNZIMConverter.convertConversationFromJsToJava(config.getMap("nextConversation"))
                    : null;
        }

        ZIMConversationFilterOption optionObj = new ZIMConversationFilterOption();
        if (option != null) {
            optionObj.isOnlyUnreadConversation =
                option.hasKey("isOnlyUnreadConversation") ? option.getBoolean("isOnlyUnreadConversation") : false;

            if (option.hasKey("marks")) {
                ArrayList<Integer> marks = new ArrayList<>();
                for (Object key : option.getArray("marks").toArrayList()) {
                    marks.add(Double.valueOf((double) key).intValue());
                }
                optionObj.marks = marks;
            }

            if (option.hasKey("conversationTypes")) {
                ArrayList<ZIMConversationType> types = new ArrayList<>();
                for (Object key : option.getArray("conversationTypes").toArrayList()) {
                    types.add(ZIMConversationType.getZIMConversationType(Double.valueOf((double) key).intValue()));
                }
                optionObj.conversationTypes = types;
            }
        }

        zim.queryConversationList(
            configObj,
            optionObj,
            new ZIMConversationListQueriedCallback() {
                @Override
                public void onConversationListQueried(ArrayList<ZIMConversation> conversationList, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        WritableArray convInfoArray = Arguments.createArray();
                        for (ZIMConversation conversation : conversationList) {
                            convInfoArray.pushMap(RNZIMConverter.convertConversation(conversation));
                        }

                        returnMap.putArray("conversationList", convInfoArray);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryConversationPinnedList(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMConversationQueryConfig configObj = new ZIMConversationQueryConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextConversation =
                config.hasKey("nextConversation")
                    ? RNZIMConverter.convertConversationFromJsToJava(config.getMap("nextConversation"))
                    : null;
        }

        zim.queryConversationPinnedList(
            configObj,
            new ZIMConversationPinnedListQueriedCallback() {
                @Override
                public void onConversationPinnedListQueried(
                    ArrayList<ZIMConversation> conversationList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        WritableArray convInfoArray = Arguments.createArray();
                        for (ZIMConversation conversation : conversationList) {
                            convInfoArray.pushMap(RNZIMConverter.convertConversation(conversation));
                        }

                        returnMap.putArray("conversationList", convInfoArray);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryConversationTotalUnreadMessageCount(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMConversationTotalUnreadMessageCountQueryConfig configObj =
            new ZIMConversationTotalUnreadMessageCountQueryConfig();
        if (config != null) {
            if (config.hasKey("marks")) {
                ArrayList<Integer> marks = new ArrayList<>();
                for (Object key : config.getArray("marks").toArrayList()) {
                    marks.add(Double.valueOf((double) key).intValue());
                }
                configObj.marks = marks;
            }

            if (config.hasKey("conversationTypes")) {
                ArrayList<ZIMConversationType> types = new ArrayList<>();
                for (Object key : config.getArray("conversationTypes").toArrayList()) {
                    types.add(ZIMConversationType.getZIMConversationType(Double.valueOf((double) key).intValue()));
                }
                configObj.conversationTypes = types;
            }
        }

        zim.queryConversationTotalUnreadMessageCount(
            configObj,
            new ZIMConversationTotalUnreadMessageCountQueriedCallback() {
                @Override
                public void onConversationTotalUnreadMessageCountQueried(int unreadMessageCount, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putInt("unreadMessageCount", unreadMessageCount);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void deleteConversation(
        String handle,
        String conversationID,
        int conversationType,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMConversationDeleteConfig configObj = new ZIMConversationDeleteConfig();
        if (config != null) {
            configObj.isAlsoDeleteServerConversation =
                config.hasKey("isAlsoDeleteServerConversation")
                    ? config.getBoolean("isAlsoDeleteServerConversation")
                    : true;
        }

        zim.deleteConversation(
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            configObj,
            new ZIMConversationDeletedCallback() {
                @Override
                public void onConversationDeleted(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void deleteAllConversations(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMConversationDeleteConfig configObj = new ZIMConversationDeleteConfig();
        if (config != null) {
            configObj.isAlsoDeleteServerConversation =
                config.hasKey("isAlsoDeleteServerConversation")
                    ? config.getBoolean("isAlsoDeleteServerConversation")
                    : true;
        }

        zim.deleteAllConversations(
            configObj,
            new ZIMConversationsAllDeletedCallback() {
                @Override
                public void onConversationsAllDeleted(ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        promise.resolve(null);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void setConversationNotificationStatus(
        String handle,
        int status,
        String conversationID,
        int conversationType,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.setConversationNotificationStatus(
            ZIMConversationNotificationStatus.getZIMConversationNotificationStatus(status),
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            new ZIMConversationNotificationStatusSetCallback() {
                @Override
                public void onConversationNotificationStatusSet(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void setConversationDraft(
        String handle,
        String draft,
        String conversationID,
        int conversationType,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.setConversationDraft(
            draft,
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            new ZIMConversationDraftSetCallback() {
                @Override
                public void onConversationDraftSet(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateConversationPinnedState(
        String handle,
        boolean isPinned,
        String conversationID,
        int conversationType,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateConversationPinnedState(
            isPinned,
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            new ZIMConversationPinnedStateUpdatedCallback() {
                @Override
                public void onConversationPinnedStateUpdated(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void setConversationMark(
        String handle,
        int markType,
        boolean enable,
        ReadableArray conversationInfos,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<ZIMConversationBaseInfo> infos = new ArrayList<>();
        for (Object info : conversationInfos.toArrayList()) {
            ZIMConversationType type = ZIMConversationType.getZIMConversationType(
                Double.valueOf((double) ((HashMap<String, Object>) info).get("conversationType")).intValue()
            );
            ZIMConversationBaseInfo infoObj = new ZIMConversationBaseInfo();
            infoObj.conversationID = (String) ((HashMap<String, Object>) info).get("conversationID");
            infoObj.conversationType = type;

            infos.add(infoObj);
        }

        zim.setConversationMark(
            markType,
            enable,
            infos,
            new ZIMConversationMarkSetCallback() {
                @Override
                public void onConversationMarkSet(
                    ArrayList<ZIMConversationBaseInfo> failedConversationInfos,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        WritableArray infoArray = Arguments.createArray();
                        for (ZIMConversationBaseInfo info : failedConversationInfos) {
                            WritableMap infoMap = Arguments.createMap();
                            infoMap.putString("conversationID", info.conversationID);
                            infoMap.putInt("conversationType", info.conversationType.value());
                            infoArray.pushMap(infoMap);
                        }
                        returnMap.putArray("failedConversationInfos", infoArray);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void clearConversationUnreadMessageCount(
        String handle,
        String conversationID,
        int conversationType,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.clearConversationUnreadMessageCount(
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            new ZIMConversationUnreadMessageCountClearedCallback() {
                @Override
                public void onConversationUnreadMessageCountCleared(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void clearConversationTotalUnreadMessageCount(String handle, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.clearConversationTotalUnreadMessageCount(
            new ZIMConversationTotalUnreadMessageCountClearedCallback() {
                @Override
                public void onConversationTotalUnreadMessageCountCleared(ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        promise.resolve(null);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void deleteAllConversationMessages(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessageDeleteConfig configObj = new ZIMMessageDeleteConfig();
        if (config != null) {
            configObj.isAlsoDeleteServerMessage =
                config.hasKey("isAlsoDeleteServerMessage") ? config.getBoolean("isAlsoDeleteServerMessage") : true;
        }

        zim.deleteAllConversationMessages(
            configObj,
            new ZIMConversationMessagesAllDeletedCallback() {
                @Override
                public void onConversationMessagesAllDeleted(ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        promise.resolve(null);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void sendMessage(
        String handle,
        ReadableMap message,
        String conversationID,
        int conversationType,
        ReadableMap config,
        final String _handle,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMConversationType type = ZIMConversationType.getZIMConversationType(conversationType);
        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);
        ZIMMessageSendConfig configObj = new ZIMMessageSendConfig();
        if (config != null) {
            configObj.priority =
                config.hasKey("priority")
                    ? ZIMMessagePriority.getZIMMessagePriority(config.getInt("priority"))
                    : ZIMMessagePriority.LOW;
            configObj.hasReceipt = config.hasKey("hasReceipt") ? config.getBoolean("hasReceipt") : false;
            configObj.isRetrySend = config.hasKey("isRetrySend") ? config.getBoolean("isRetrySend") : false;
            configObj.isNotifyMentionedUsers =
                config.hasKey("isNotifyMentionedUsers") ? config.getBoolean("isNotifyMentionedUsers") : false;
            configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);
        }

        zim.sendMessage(
            messageObj,
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            configObj,
            new ZIMMessageSentFullCallback() {
                @Override
                public void onMessageSent(ZIMMessage message, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        WritableMap messageMap = RNZIMConverter.convertMessage(message);
                        returnMap.putMap("message", messageMap);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }

                @Override
                public void onMessageAttached(ZIMMessage message) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushMap(RNZIMConverter.convertMessage(message));
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messageAttachedHook", paramsMap);
                }

                @Override
                public void onMediaUploadingProgress(
                    ZIMMediaMessage message,
                    long currentFileSize,
                    long totalFileSize
                ) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushMap(RNZIMConverter.convertMessage(message));
                    dataArray.pushDouble(currentFileSize);
                    dataArray.pushDouble(totalFileSize);
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messageLoadingProgress", paramsMap);
                }

                @Override
                public void onMultipleMediaUploadingProgress(
                    ZIMMultipleMessage message,
                    long currentFileSize,
                    long totalFileSize,
                    int messageInfoIndex,
                    long currentIndexFileSize,
                    long totalIndexFileSize
                ) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushMap(RNZIMConverter.convertMessage(message));
                    dataArray.pushDouble(currentFileSize);
                    dataArray.pushDouble(totalFileSize);
                    dataArray.pushDouble(messageInfoIndex);
                    dataArray.pushDouble(currentIndexFileSize);
                    dataArray.pushDouble(totalIndexFileSize);
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messageLoadingProgress", paramsMap);
                }
            }
        );
    }

    @ReactMethod
    public void editMessage(
        String handle,
        ReadableMap message,
        final String _handle,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);
        ZIMMessageEditConfig configObj = new ZIMMessageEditConfig();

        zim.editMessage(
            messageObj,
            configObj,
            new ZIMMessageSentFullCallback() {
                @Override
                public void onMessageSent(ZIMMessage message, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        WritableMap messageMap = RNZIMConverter.convertMessage(message);
                        returnMap.putMap("message", messageMap);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }

                @Override
                public void onMessageAttached(ZIMMessage message) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushMap(RNZIMConverter.convertMessage(message));
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messageAttachedHook", paramsMap);
                }

                @Override
                public void onMediaUploadingProgress(
                    ZIMMediaMessage message,
                    long currentFileSize,
                    long totalFileSize
                ) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushMap(RNZIMConverter.convertMessage(message));
                    dataArray.pushDouble(currentFileSize);
                    dataArray.pushDouble(totalFileSize);
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messageLoadingProgress", paramsMap);
                }

                @Override
                public void onMultipleMediaUploadingProgress(
                    ZIMMultipleMessage message,
                    long currentFileSize,
                    long totalFileSize,
                    int messageInfoIndex,
                    long currentIndexFileSize,
                    long totalIndexFileSize
                ) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushMap(RNZIMConverter.convertMessage(message));
                    dataArray.pushDouble(currentFileSize);
                    dataArray.pushDouble(totalFileSize);
                    dataArray.pushDouble(messageInfoIndex);
                    dataArray.pushDouble(currentIndexFileSize);
                    dataArray.pushDouble(totalIndexFileSize);
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messageLoadingProgress", paramsMap);
                }
            }
        );
    }

    @ReactMethod
    public void deleteMessages(
        String handle,
        ReadableArray messageList,
        String conversationID,
        int conversationType,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<ZIMMessage> zimMessageArrayList = new ArrayList<>();
        for (Object messageObj : messageList.toArrayList()) {
            ZIMMessage message = RNZIMConverter.convertMessageFromJsToJava((HashMap<String, Object>) messageObj);
            zimMessageArrayList.add(message);
        }

        ZIMMessageDeleteConfig configObj = new ZIMMessageDeleteConfig();
        if (config != null) {
            configObj.isAlsoDeleteServerMessage =
                config.hasKey("isAlsoDeleteServerMessage") ? config.getBoolean("isAlsoDeleteServerMessage") : true;
        }

        zim.deleteMessages(
            zimMessageArrayList,
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            configObj,
            new ZIMMessageDeletedCallback() {
                @Override
                public void onMessageDeleted(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void deleteAllMessage(
        String handle,
        String conversationID,
        int conversationType,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessageDeleteConfig configObj = new ZIMMessageDeleteConfig();
        if (config != null) {
            configObj.isAlsoDeleteServerMessage =
                config.hasKey("isAlsoDeleteServerMessage") ? config.getBoolean("isAlsoDeleteServerMessage") : true;
        }

        zim.deleteAllMessage(
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            configObj,
            new ZIMMessageDeletedCallback() {
                @Override
                public void onMessageDeleted(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryHistoryMessage(
        String handle,
        String conversationID,
        int conversationType,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessageQueryConfig configObj = new ZIMMessageQueryConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.reverse = config.hasKey("reverse") ? config.getBoolean("reverse") : false;
            configObj.nextMessage =
                config.hasKey("nextMessage")
                    ? RNZIMConverter.convertMessageFromJsToJava(config.getMap("nextMessage"))
                    : null;
        }

        zim.queryHistoryMessage(
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            configObj,
            new ZIMMessageQueriedCallback() {
                @Override
                public void onMessageQueried(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ArrayList<ZIMMessage> messageList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());
                        returnMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void downloadMediaFile(
        String handle,
        ReadableMap message,
        int fileType,
        ReadableMap config,
        final String _handle,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);
        ZIMMediaDownloadConfig configObj = new ZIMMediaDownloadConfig();
        configObj.messageInfoIndex = config.hasKey("messageInfoIndex") ? config.getInt("messageInfoIndex") : 0;

        zim.downloadMediaFile(
            messageObj,
            ZIMMediaFileType.getZIMMediaFileType(fileType),
            configObj,
            new ZIMMediaDownloadedCallback() {
                @Override
                public void onMediaDownloaded(ZIMMessage message, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        WritableMap messageMap = RNZIMConverter.convertMessage(message);
                        returnMap.putMap("message", messageMap);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }

                @Override
                public void onMediaDownloadingProgress(
                    ZIMMessage message,
                    long currentFileSize,
                    long totalFileSize
                ) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushMap(RNZIMConverter.convertMessage(message));
                    dataArray.pushDouble(currentFileSize);
                    dataArray.pushDouble(totalFileSize);
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messageLoadingProgress", paramsMap);
                }
            }
        );
    }

    @ReactMethod
    public void importLocalMessages(String handle, String folderPath, final String _handle, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.importLocalMessages(
            folderPath,
            new ZIMMessageImportConfig(),
            new ZIMMessageImportedCallback() {
                @Override
                public void onMessageImported(ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        promise.resolve(null);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }

                @Override
                public void onMessageImportingProgress(long currentMessageCount, long totalMessageCount) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushDouble(currentMessageCount);
                    dataArray.pushDouble(totalMessageCount);
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messagePortProgress", paramsMap);
                }
            }
        );
    }

    @ReactMethod
    public void exportLocalMessages(String handle, String folderPath, final String _handle, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.exportLocalMessages(
            folderPath,
            new ZIMMessageExportConfig(),
            new ZIMMessageExportedCallback() {
                @Override
                public void onMessageExported(ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        promise.resolve(null);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }

                @Override
                public void onMessageExportingProgress(long currentMessageCount, long totalMessageCount) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushDouble(currentMessageCount);
                    dataArray.pushDouble(totalMessageCount);
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messagePortProgress", paramsMap);
                }
            }
        );
    }

    @ReactMethod
    public void insertMessageToLocalDB(
        String handle,
        ReadableMap message,
        String conversationID,
        int conversationType,
        String senderUserID,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);
        zim.insertMessageToLocalDB(
            messageObj,
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            senderUserID,
            new ZIMMessageInsertedCallback() {
                @Override
                public void onMessageInserted(ZIMMessage message, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        WritableMap messageMap = RNZIMConverter.convertMessage(message);
                        returnMap.putMap("message", messageMap);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateMessageLocalExtendedData(
        String handle,
        String localExtendedData,
        ReadableMap message,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);
        zim.updateMessageLocalExtendedData(
            localExtendedData,
            messageObj,
            new ZIMMessageLocalExtendedDataUpdatedCallback() {
                @Override
                public void onMessageExtendedDataUpdated(ZIMMessage message, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        WritableMap messageMap = RNZIMConverter.convertMessage(message);
                        returnMap.putMap("message", messageMap);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void sendConversationMessageReceiptRead(
        String handle,
        String conversationID,
        int conversationType,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.sendConversationMessageReceiptRead(
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            new ZIMConversationMessageReceiptReadSentCallback() {
                @Override
                public void onConversationMessageReceiptReadSent(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void sendMessageReceiptsRead(
        String handle,
        ReadableArray messageList,
        String conversationID,
        int conversationType,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<ZIMMessage> zimMessageArrayList = new ArrayList<>();
        for (Object messageObj : messageList.toArrayList()) {
            ZIMMessage message = RNZIMConverter.convertMessageFromJsToJava((HashMap<String, Object>) messageObj);
            zimMessageArrayList.add(message);
        }

        zim.sendMessageReceiptsRead(
            zimMessageArrayList,
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            new ZIMMessageReceiptsReadSentCallback() {
                @Override
                public void onMessageReceiptsReadSent(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ArrayList<Long> errorMessageIDs,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());

                        WritableArray errorMessageIDsArray = Arguments.createArray();
                        for (long key : errorMessageIDs) {
                            errorMessageIDsArray.pushString(String.valueOf(key));
                        }
                        returnMap.putArray("errorMessageIDs", errorMessageIDsArray);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryMessageReceiptsInfo(
        String handle,
        ReadableArray messageList,
        String conversationID,
        int conversationType,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<ZIMMessage> zimMessageArrayList = new ArrayList<>();
        for (Object messageObj : messageList.toArrayList()) {
            ZIMMessage message = RNZIMConverter.convertMessageFromJsToJava((HashMap<String, Object>) messageObj);
            zimMessageArrayList.add(message);
        }

        zim.queryMessageReceiptsInfo(
            zimMessageArrayList,
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            new ZIMMessageReceiptsInfoQueriedCallback() {
                @Override
                public void onMessageReceiptsInfoQueried(
                    ArrayList<ZIMMessageReceiptInfo> infos,
                    ArrayList<Long> errorMessageIDs,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        returnMap.putArray("infos", RNZIMConverter.convertMessageReceiptInfoList(infos));

                        WritableArray errorMessageIDsArray = Arguments.createArray();
                        for (long key : errorMessageIDs) {
                            errorMessageIDsArray.pushString(String.valueOf(key));
                        }
                        returnMap.putArray("errorMessageIDs", errorMessageIDsArray);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryGroupMessageReceiptMemberList(
        String handle,
        ReadableMap message,
        String groupID,
        ReadableMap config,
        boolean read,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);
        ZIMGroupMessageReceiptMemberQueryConfig configObj = new ZIMGroupMessageReceiptMemberQueryConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextFlag = config.hasKey("nextFlag") ? config.getInt("nextFlag") : 0;
        }

        ZIMGroupMessageReceiptMemberListQueriedCallback callback =
            new ZIMGroupMessageReceiptMemberListQueriedCallback() {
                @Override
                public void onGroupMessageReceiptMemberListQueried(
                    String groupID,
                    ArrayList<ZIMGroupMemberInfo> userList,
                    int nextFlag,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putInt("nextFlag", nextFlag);
                        returnMap.putArray("userList", RNZIMConverter.convertGroupMemberInfoList(userList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            };

        if (read) {
            zim.queryGroupMessageReceiptReadMemberList(messageObj, groupID, configObj, callback);
        } else {
            zim.queryGroupMessageReceiptUnreadMemberList(messageObj, groupID, configObj, callback);
        }
    }

    @ReactMethod
    public void revokeMessage(String handle, ReadableMap message, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);
        ZIMMessageRevokeConfig configObj = new ZIMMessageRevokeConfig();
        if (config != null) {
            configObj.revokeExtendedData =
                config.hasKey("revokeExtendedData") ? config.getString("revokeExtendedData") : "";
            configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);
        }

        zim.revokeMessage(
            messageObj,
            configObj,
            new ZIMMessageRevokedCallback() {
                @Override
                public void onMessageRevoked(ZIMMessage message, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("message", RNZIMConverter.convertMessage(message));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryCombineMessageDetail(String handle, ReadableMap message, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMCombineMessage messageObj = (ZIMCombineMessage) RNZIMConverter.convertMessageFromJsToJava(message);

        zim.queryCombineMessageDetail(
            messageObj,
            new ZIMCombineMessageDetailQueriedCallback() {
                @Override
                public void onCombineMessageDetailQueried(ZIMCombineMessage message, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("message", RNZIMConverter.convertMessage(message));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void addMessageReaction(String handle, String reactionType, ReadableMap message, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);

        zim.addMessageReaction(
            reactionType,
            messageObj,
            new ZIMMessageReactionAddedCallback() {
                @Override
                public void onMessageReactionAdded(ZIMMessageReaction reaction, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("reaction", RNZIMConverter.convertMessageReaction(reaction));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void deleteMessageReaction(String handle, String reactionType, ReadableMap message, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);

        zim.deleteMessageReaction(
            reactionType,
            messageObj,
            new ZIMMessageReactionDeletedCallback() {
                @Override
                public void onMessageReactionDeleted(ZIMMessageReaction reaction, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("reaction", RNZIMConverter.convertMessageReaction(reaction));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryMessageReactionUserList(
        String handle,
        ReadableMap message,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);
        ZIMMessageReactionUserQueryConfig configObj = new ZIMMessageReactionUserQueryConfig();
        if (config != null) {
            configObj.reactionType = config.hasKey("reactionType") ? config.getString("reactionType") : "";
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextFlag =
                config.hasKey("nextFlag") ? Double.valueOf(config.getDouble("nextFlag")).longValue() : 0;
        }

        zim.queryMessageReactionUserList(
            messageObj,
            configObj,
            new ZIMMessageReactionUserListQueriedCallback() {
                @Override
                public void onMessageReactionUserListQueried(
                    ZIMMessage message,
                    ArrayList<ZIMMessageReactionUserInfo> userList,
                    String reactionType,
                    long nextFlag,
                    int totalCount,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("reactionType", reactionType);
                        returnMap.putDouble("nextFlag", nextFlag);
                        returnMap.putInt("totalCount", totalCount);

                        WritableArray userArray = Arguments.createArray();
                        for (ZIMMessageReactionUserInfo user : userList) {
                            WritableMap userMap = Arguments.createMap();
                            userMap.putString("userID", user.userID);
                            userArray.pushMap(userMap);
                        }
                        returnMap.putArray("userList", userArray);

                        returnMap.putMap("message", RNZIMConverter.convertMessage(message));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void replyMessage(
        String handle,
        ReadableMap message,
        ReadableMap originMessage,
        ReadableMap config,
        final String _handle,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);
        ZIMMessage originMessageObj = RNZIMConverter.convertMessageFromJsToJava(originMessage);

        ZIMMessageSendConfig configObj = new ZIMMessageSendConfig();
        if (config != null) {
            configObj.priority =
                config.hasKey("priority")
                    ? ZIMMessagePriority.getZIMMessagePriority(config.getInt("priority"))
                    : ZIMMessagePriority.LOW;
            configObj.hasReceipt = config.hasKey("hasReceipt") ? config.getBoolean("hasReceipt") : false;
            configObj.isRetrySend = config.hasKey("isRetrySend") ? config.getBoolean("isRetrySend") : false;
            configObj.isNotifyMentionedUsers =
                config.hasKey("isNotifyMentionedUsers") ? config.getBoolean("isNotifyMentionedUsers") : false;
            configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);
        }

        zim.replyMessage(
            messageObj,
            originMessageObj,
            configObj,
            new ZIMMessageSentFullCallback() {
                @Override
                public void onMessageSent(ZIMMessage message, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("message", RNZIMConverter.convertMessage(message));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }

                @Override
                public void onMessageAttached(ZIMMessage message) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushMap(RNZIMConverter.convertMessage(message));
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messageAttachedHook", paramsMap);
                }

                @Override
                public void onMediaUploadingProgress(ZIMMediaMessage message, long currentFileSize, long totalFileSize) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushMap(RNZIMConverter.convertMessage(message));
                    dataArray.pushDouble(currentFileSize);
                    dataArray.pushDouble(totalFileSize);
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messageLoadingProgress", paramsMap);
                }

                @Override
                public void onMultipleMediaUploadingProgress(
                    ZIMMultipleMessage message,
                    long currentFileSize,
                    long totalFileSize,
                    int messageInfoIndex,
                    long currentIndexFileSize,
                    long totalIndexFileSize
                ) {
                    WritableMap paramsMap = Arguments.createMap();
                    paramsMap.putString("handle", _handle);

                    WritableArray dataArray = Arguments.createArray();
                    dataArray.pushMap(RNZIMConverter.convertMessage(message));
                    dataArray.pushDouble(currentFileSize);
                    dataArray.pushDouble(totalFileSize);
                    dataArray.pushDouble(messageInfoIndex);
                    dataArray.pushDouble(currentIndexFileSize);
                    dataArray.pushDouble(totalIndexFileSize);
                    paramsMap.putArray("data", dataArray);

                    sendAPIEvent("messageLoadingProgress", paramsMap);
                }
            }
        );
    }

    @ReactMethod
    public void queryMessageRepliedList(String handle, ReadableMap message, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessage messageObj = RNZIMConverter.convertMessageFromJsToJava(message);
        ZIMMessageRepliedListQueryConfig configObj = new ZIMMessageRepliedListQueryConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextFlag =
                config.hasKey("nextFlag") ? Double.valueOf(config.getDouble("nextFlag")).longValue() : 0;
        }

        zim.queryMessageRepliedList(
            messageObj,
            configObj,
            new ZIMMessageRepliedListQueriedCallback() {
                @Override
                public void onMessageRepliedListQueried(
                    ArrayList<ZIMMessage> messageList,
                    long nextFlag,
                    ZIMMessageRootRepliedInfo rootRepliedInfo,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap infoMap = Arguments.createMap();
                        infoMap.putInt("state", rootRepliedInfo.state.value());
                        infoMap.putDouble("repliedCount", rootRepliedInfo.repliedCount);
                        infoMap.putString("senderUserID", rootRepliedInfo.senderUserID);
                        infoMap.putDouble("sentTime", rootRepliedInfo.sentTime);
                        if (rootRepliedInfo.message != null) {
                            infoMap.putMap("message", RNZIMConverter.convertMessage(rootRepliedInfo.message));
                        }

                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putDouble("nextFlag", nextFlag);
                        returnMap.putMap("rootRepliedInfo", infoMap);
                        returnMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryMessages(
        String handle,
        ReadableArray messageSeqs,
        String conversationID,
        int conversationType,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<Long> messageSeqList = new ArrayList<>();
        for (Object key : messageSeqs.toArrayList()) {
            messageSeqList.add(Double.valueOf((double) key).longValue());
        }

        zim.queryMessages(
            messageSeqList,
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            new ZIMMessageQueriedCallback() {
                @Override
                public void onMessageQueried(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ArrayList<ZIMMessage> messageList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());
                        returnMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void createRoom(String handle, ReadableMap roomInfo, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMRoomCreatedCallback callback = new ZIMRoomCreatedCallback() {
            @Override
            public void onRoomCreated(ZIMRoomFullInfo roomInfo, ZIMError errorInfo) {
                if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                    WritableMap returnMap = Arguments.createMap();
                    returnMap.putMap("roomInfo", RNZIMConverter.convertFullRoomInfo(roomInfo));
                    promise.resolve(returnMap);
                } else {
                    promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                }
            }
        };

        ZIMRoomInfo roomInfoObj = new ZIMRoomInfo();
        roomInfoObj.roomID = roomInfo.getString("roomID");
        roomInfoObj.roomName = roomInfo.hasKey("roomName") ? roomInfo.getString("roomName") : "";

        if (config != null) {
            ZIMRoomAdvancedConfig configObj = new ZIMRoomAdvancedConfig();
            configObj.roomDestroyDelayTime =
                config.hasKey("roomDestroyDelayTime") ? config.getInt("roomDestroyDelayTime") : 0;

            if (config.hasKey("roomAttributes")) {
                HashMap<String, String> roomAttributes = new HashMap<>();
                HashMap<String, Object> roomAttrMap = config.getMap("roomAttributes").toHashMap();
                for (String key : roomAttrMap.keySet()) {
                    roomAttributes.put(key, (String) roomAttrMap.get(key));
                }

                configObj.roomAttributes = roomAttributes;
            }

            zim.createRoom(roomInfoObj, configObj, callback);
        } else {
            zim.createRoom(roomInfoObj, callback);
        }
    }

    @ReactMethod
    public void enterRoom(String handle, ReadableMap roomInfo, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMRoomInfo roomInfoObj = new ZIMRoomInfo();
        roomInfoObj.roomID = roomInfo.getString("roomID");
        roomInfoObj.roomName = roomInfo.hasKey("roomName") ? roomInfo.getString("roomName") : "";

        ZIMRoomAdvancedConfig configObj = null;
        if (config != null) {
            configObj = new ZIMRoomAdvancedConfig();
            configObj.roomDestroyDelayTime =
                config.hasKey("roomDestroyDelayTime") ? config.getInt("roomDestroyDelayTime") : 0;

            if (config.hasKey("roomAttributes")) {
                HashMap<String, String> roomAttributes = new HashMap<>();
                HashMap<String, Object> roomAttrMap = config.getMap("roomAttributes").toHashMap();
                for (String key : roomAttrMap.keySet()) {
                    roomAttributes.put(key, (String) roomAttrMap.get(key));
                }

                configObj.roomAttributes = roomAttributes;
            }
        }

        zim.enterRoom(
            roomInfoObj,
            configObj,
            new ZIMRoomEnteredCallback() {
                @Override
                public void onRoomEntered(ZIMRoomFullInfo roomInfo, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("roomInfo", RNZIMConverter.convertFullRoomInfo(roomInfo));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void joinRoom(String handle, String roomID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.joinRoom(
            roomID,
            new ZIMRoomJoinedCallback() {
                @Override
                public void onRoomJoined(ZIMRoomFullInfo roomInfo, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("roomInfo", RNZIMConverter.convertFullRoomInfo(roomInfo));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void switchRoom(
        String handle,
        String fromRoomID,
        ReadableMap roomInfo,
        boolean isCreate,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMRoomSwitchedCallback callback = new ZIMRoomSwitchedCallback() {
            @Override
            public void onRoomSwitched(ZIMRoomFullInfo roomInfo, ZIMError errorInfo) {
                if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                    WritableMap returnMap = Arguments.createMap();
                    returnMap.putMap("roomInfo", RNZIMConverter.convertFullRoomInfo(roomInfo));
                    promise.resolve(returnMap);
                } else {
                    promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                }
            }
        };

        ZIMRoomInfo roomInfoObj = new ZIMRoomInfo();
        roomInfoObj.roomID = roomInfo.getString("roomID");
        roomInfoObj.roomName = roomInfo.hasKey("roomName") ? roomInfo.getString("roomName") : "";

        ZIMRoomAdvancedConfig configObj = new ZIMRoomAdvancedConfig();
        if (config != null) {
            configObj.roomDestroyDelayTime =
                config.hasKey("roomDestroyDelayTime") ? config.getInt("roomDestroyDelayTime") : 0;

            if (config.hasKey("roomAttributes")) {
                HashMap<String, String> roomAttributes = new HashMap<>();
                HashMap<String, Object> roomAttrMap = config.getMap("roomAttributes").toHashMap();
                for (String key : roomAttrMap.keySet()) {
                    roomAttributes.put(key, (String) roomAttrMap.get(key));
                }

                configObj.roomAttributes = roomAttributes;
            }
        }

        zim.switchRoom(fromRoomID, roomInfoObj, isCreate, configObj, callback);
    }

    @ReactMethod
    public void leaveRoom(String handle, String roomID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.leaveRoom(
            roomID,
            new ZIMRoomLeftCallback() {
                @Override
                public void onRoomLeft(String roomID, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("roomID", roomID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void leaveAllRoom(String handle, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.leaveAllRoom(
            new ZIMRoomAllLeftCallback() {
                @Override
                public void onRoomAllLeft(ArrayList<String> roomIDs, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        WritableArray roomIDArray = Arguments.createArray();
                        for (String roomID : roomIDs) {
                            roomIDArray.pushString(roomID);
                        }
                        returnMap.putArray("roomIDs", roomIDArray);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryRoomMemberList(String handle, String roomID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMRoomMemberQueryConfig configObj = new ZIMRoomMemberQueryConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextFlag = config.hasKey("nextFlag") ? config.getString("nextFlag") : "";
        }

        zim.queryRoomMemberList(
            roomID,
            configObj,
            new ZIMRoomMemberQueriedCallback() {
                @Override
                public void onRoomMemberQueried(
                    String roomID,
                    ArrayList<ZIMUserInfo> memberList,
                    String nextFlag,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        returnMap.putString("roomID", roomID);
                        returnMap.putString("nextFlag", nextFlag);
                        returnMap.putArray("memberList", RNZIMConverter.convertUserInfoList(memberList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryRoomMembers(String handle, ReadableArray userIDs, String roomID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userIDArray = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userIDArray.add((String) userID);
        }

        zim.queryRoomMembers(
            userIDArray,
            roomID,
            new ZIMRoomMembersQueriedCallback() {
                @Override
                public void onRoomMembersQueried(
                    String roomID,
                    ArrayList<ZIMRoomMemberInfo> memberList,
                    ArrayList<ZIMErrorUserInfo> errorUserList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        WritableArray memberArray = Arguments.createArray();
                        for (ZIMRoomMemberInfo userInfo : memberList) {
                            memberArray.pushMap(RNZIMConverter.convertRoomMemberInfo(userInfo));
                        }

                        returnMap.putString("roomID", roomID);
                        returnMap.putArray("memberList", memberArray);
                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryRoomOnlineMemberCount(String handle, String roomID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.queryRoomOnlineMemberCount(
            roomID,
            new ZIMRoomOnlineMemberCountQueriedCallback() {
                @Override
                public void onRoomOnlineMemberCountQueried(String roomID, int count, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("roomID", roomID);
                        returnMap.putInt("count", count);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryRoomAllAttributes(String handle, String roomID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.queryRoomAllAttributes(
            roomID,
            new ZIMRoomAttributesQueriedCallback() {
                @Override
                public void onRoomAttributesQueried(
                    String roomID,
                    HashMap<String, String> roomAttributes,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        WritableMap roomAttrMap = Arguments.createMap();
                        for (String key : roomAttributes.keySet()) {
                            roomAttrMap.putString(key, roomAttributes.get(key));
                        }
                        returnMap.putString("roomID", roomID);
                        returnMap.putMap("roomAttributes", roomAttrMap);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void setRoomAttributes(
        String handle,
        ReadableMap roomAttributes,
        String roomID,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        HashMap<String, String> roomAttrsMap = new HashMap<>();
        for (String key : roomAttributes.toHashMap().keySet()) {
            roomAttrsMap.put(key, roomAttributes.getString(key));
        }

        ZIMRoomAttributesSetConfig configObj = new ZIMRoomAttributesSetConfig();
        if (config != null) {
            configObj.isUpdateOwner = config.hasKey("isUpdateOwner") ? config.getBoolean("isUpdateOwner") : false;
            configObj.isDeleteAfterOwnerLeft =
                config.hasKey("isDeleteAfterOwnerLeft") ? config.getBoolean("isDeleteAfterOwnerLeft") : false;
            configObj.isForce = config.hasKey("isForce") ? config.getBoolean("isForce") : false;
        }

        zim.setRoomAttributes(
            roomAttrsMap,
            roomID,
            configObj,
            new ZIMRoomAttributesOperatedCallback() {
                @Override
                public void onRoomAttributesOperated(String roomID, ArrayList<String> errorKeys, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("roomID", roomID);
                        WritableArray errorKeysArray = Arguments.createArray();
                        for (String key : errorKeys) {
                            errorKeysArray.pushString(key);
                        }
                        returnMap.putArray("errorKeys", errorKeysArray);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void deleteRoomAttributes(
        String handle,
        ReadableArray keys,
        String roomID,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> keysArray = new ArrayList<>();
        for (Object key : keys.toArrayList()) {
            keysArray.add((String) key);
        }

        ZIMRoomAttributesDeleteConfig configObj = new ZIMRoomAttributesDeleteConfig();
        if (config != null) {
            configObj.isForce = config.hasKey("isForce") ? config.getBoolean("isForce") : false;
        }

        zim.deleteRoomAttributes(
            keysArray,
            roomID,
            configObj,
            new ZIMRoomAttributesOperatedCallback() {
                @Override
                public void onRoomAttributesOperated(String roomID, ArrayList<String> errorKeys, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("roomID", roomID);
                        WritableArray errorKeysArray = Arguments.createArray();
                        for (String key : errorKeys) {
                            errorKeysArray.pushString(key);
                        }
                        returnMap.putArray("errorKeys", errorKeysArray);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void beginRoomAttributesBatchOperation(String handle, String roomID, ReadableMap config) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            return;
        }

        ZIMRoomAttributesBatchOperationConfig configObj = new ZIMRoomAttributesBatchOperationConfig();
        if (config != null) {
            configObj.isUpdateOwner = config.hasKey("isUpdateOwner") ? config.getBoolean("isUpdateOwner") : false;
            configObj.isDeleteAfterOwnerLeft =
                config.hasKey("isDeleteAfterOwnerLeft") ? config.getBoolean("isDeleteAfterOwnerLeft") : false;
            configObj.isForce = config.hasKey("isForce") ? config.getBoolean("isForce") : false;
        }

        zim.beginRoomAttributesBatchOperation(roomID, configObj);
    }

    @ReactMethod
    public void endRoomAttributesBatchOperation(String handle, String roomID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.endRoomAttributesBatchOperation(
            roomID,
            new ZIMRoomAttributesBatchOperatedCallback() {
                @Override
                public void onRoomAttributesBatchOperated(String roomID, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("roomID", roomID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void setRoomMembersAttributes(
        String handle,
        ReadableMap attributes,
        ReadableArray userIDs,
        String roomID,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMRoomMemberAttributesSetConfig configObj = new ZIMRoomMemberAttributesSetConfig();
        configObj.isDeleteAfterOwnerLeft =
            config.hasKey("isDeleteAfterOwnerLeft") ? config.getBoolean("isDeleteAfterOwnerLeft") : true;

        HashMap<String, String> roomMembersAttrsMap = new HashMap<>();
        for (String key : attributes.toHashMap().keySet()) {
            roomMembersAttrsMap.put(key, attributes.getString(key));
        }

        ArrayList<String> userIDArray = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userIDArray.add((String) userID);
        }

        zim.setRoomMembersAttributes(
            roomMembersAttrsMap,
            userIDArray,
            roomID,
            configObj,
            new ZIMRoomMembersAttributesOperatedCallback() {
                @Override
                public void onRoomMembersAttributesOperated(
                    String roomID,
                    ArrayList<ZIMRoomMemberAttributesOperatedInfo> infos,
                    ArrayList<String> errorUserList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("roomID", roomID);

                        ReadableArray infoArray = RNZIMConverter.convertRoomMemberAttributesOperatedInfoList(infos);
                        returnMap.putArray("infos", infoArray);

                        WritableArray errorUserArray = Arguments.createArray();
                        for (String key : errorUserList) {
                            errorUserArray.pushString(key);
                        }

                        returnMap.putArray("errorUserList", errorUserArray);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryRoomMembersAttributes(String handle, ReadableArray userIDs, String roomID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userIDArray = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userIDArray.add((String) userID);
        }

        zim.queryRoomMembersAttributes(
            userIDArray,
            roomID,
            new ZIMRoomMembersAttributesQueriedCallback() {
                @Override
                public void onRoomMembersAttributesQueried(
                    String roomID,
                    ArrayList<ZIMRoomMemberAttributesInfo> infos,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("roomID", roomID);

                        ReadableArray infoArray = RNZIMConverter.convertRoomMemberAttributesInfoList(infos);
                        returnMap.putArray("infos", infoArray);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryRoomMemberAttributesList(String handle, String roomID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMRoomMemberAttributesQueryConfig configObj = new ZIMRoomMemberAttributesQueryConfig();
        configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
        configObj.nextFlag = config.hasKey("nextFlag") ? config.getString("nextFlag") : "";

        zim.queryRoomMemberAttributesList(
            roomID,
            configObj,
            new ZIMRoomMemberAttributesListQueriedCallback() {
                @Override
                public void onRoomMemberAttributesListQueried(
                    String roomID,
                    ArrayList<ZIMRoomMemberAttributesInfo> infos,
                    String nextFlag,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("roomID", roomID);

                        ReadableArray infoArray = RNZIMConverter.convertRoomMemberAttributesInfoList(infos);
                        returnMap.putArray("infos", infoArray);

                        returnMap.putString("nextFlag", nextFlag);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void createGroup(
        String handle,
        ReadableMap groupInfo,
        ReadableArray userIDs,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupCreatedCallback callback = new ZIMGroupCreatedCallback() {
            @Override
            public void onGroupCreated(
                ZIMGroupFullInfo groupInfo,
                ArrayList<ZIMGroupMemberInfo> userList,
                ArrayList<ZIMErrorUserInfo> errorUserList,
                ZIMError errorInfo
            ) {
                if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                    WritableMap returnMap = Arguments.createMap();
                    returnMap.putMap("groupInfo", RNZIMConverter.convertGroupFullInfo(groupInfo));
                    returnMap.putArray("userList", RNZIMConverter.convertGroupMemberInfoList(userList));
                    returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));
                    promise.resolve(returnMap);
                } else {
                    promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                }
            }
        };

        ZIMGroupInfo groupInfoObj = new ZIMGroupInfo();
        groupInfoObj.groupID = groupInfo.getString("groupID");
        groupInfoObj.groupName = groupInfo.hasKey("groupName") ? groupInfo.getString("groupName") : "";
        groupInfoObj.groupAvatarUrl = groupInfo.hasKey("groupAvatarUrl") ? groupInfo.getString("groupAvatarUrl") : "";

        ArrayList<String> userIDArray = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userIDArray.add((String) userID);
        }

        if (config != null) {
            ZIMGroupAdvancedConfig configObj = new ZIMGroupAdvancedConfig();
            configObj.groupNotice = config.hasKey("groupNotice") ? config.getString("groupNotice") : "";
            configObj.maxMemberCount = config.hasKey("maxMemberCount") ? config.getInt("maxMemberCount") : 0;
            configObj.joinMode =
                ZIMGroupJoinMode.getZIMGroupJoinMode(config.hasKey("joinMode") ? config.getInt("joinMode") : 0);
            configObj.inviteMode =
                ZIMGroupInviteMode.getZIMGroupInviteMode(config.hasKey("inviteMode") ? config.getInt("inviteMode") : 0);
            configObj.beInviteMode =
                ZIMGroupBeInviteMode.getZIMGroupBeInviteMode(
                    config.hasKey("beInviteMode") ? config.getInt("beInviteMode") : 0
                );

            if (config.hasKey("groupAttributes")) {
                HashMap<String, String> groupAttributes = new HashMap<>();
                HashMap<String, Object> groupAttrMap = config.getMap("groupAttributes").toHashMap();
                for (String key : groupAttrMap.keySet()) {
                    groupAttributes.put(key, (String) groupAttrMap.get(key));
                }

                configObj.groupAttributes = groupAttributes;
            }

            zim.createGroup(groupInfoObj, userIDArray, configObj, callback);
        } else {
            zim.createGroup(groupInfoObj, userIDArray, callback);
        }
    }

    @ReactMethod
    public void joinGroup(String handle, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.joinGroup(
            groupID,
            new ZIMGroupJoinedCallback() {
                @Override
                public void onGroupJoined(ZIMGroupFullInfo groupInfo, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("groupInfo", RNZIMConverter.convertGroupFullInfo(groupInfo));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void leaveGroup(String handle, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.leaveGroup(
            groupID,
            new ZIMGroupLeftCallback() {
                @Override
                public void onGroupLeft(String groupID, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void dismissGroup(String handle, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.dismissGroup(
            groupID,
            new ZIMGroupDismissedCallback() {
                @Override
                public void onGroupDismissed(String groupID, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryGroupList(String handle, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.queryGroupList(
            new ZIMGroupListQueriedCallback() {
                @Override
                public void onGroupListQueried(ArrayList<ZIMGroup> groupList, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putArray("groupList", RNZIMConverter.convertGroupList(groupList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateGroupNotice(String handle, String groupNotice, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateGroupNotice(
            groupNotice,
            groupID,
            new ZIMGroupNoticeUpdatedCallback() {
                @Override
                public void onGroupNoticeUpdated(String groupID, String groupNotice, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putString("groupNotice", groupNotice);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateGroupName(String handle, String groupName, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateGroupName(
            groupName,
            groupID,
            new ZIMGroupNameUpdatedCallback() {
                @Override
                public void onGroupNameUpdated(String groupID, String groupName, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putString("groupName", groupName);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateGroupAvatarUrl(String handle, String groupAvatarUrl, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateGroupAvatarUrl(
            groupAvatarUrl,
            groupID,
            new ZIMGroupAvatarUrlUpdatedCallback() {
                @Override
                public void onGroupAvatarUrlUpdated(String groupID, String groupAvatarUrl, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putString("groupAvatarUrl", groupAvatarUrl);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void muteGroup(String handle, boolean isMute, String groupID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupMuteConfig configObj = new ZIMGroupMuteConfig();
        configObj.mode = ZIMGroupMuteMode.getZIMGroupMuteMode(config.hasKey("mode") ? config.getInt("mode") : 0);
        configObj.duration = config.hasKey("duration") ? config.getInt("duration") : 0;
        if (config.hasKey("roles")) {
            ArrayList<Integer> roles = new ArrayList<>();
            for (Object key : config.getArray("roles").toArrayList()) {
                roles.add(Double.valueOf((double) key).intValue());
            }
            configObj.roles = roles;
        }

        zim.muteGroup(
            isMute,
            groupID,
            configObj,
            new ZIMGroupMutedCallback() {
                @Override
                public void onGroupMuted(String groupID, boolean isMute, ZIMGroupMuteInfo info, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putBoolean("isMute", isMute);
                        returnMap.putMap("mutedInfo", RNZIMConverter.convertGroupMuteInfo(info));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void muteGroupMembers(
        String handle,
        boolean isMute,
        ReadableArray userIDs,
        String groupID,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupMemberMuteConfig configObj = new ZIMGroupMemberMuteConfig();
        configObj.duration = config.hasKey("duration") ? config.getInt("duration") : 0;

        ArrayList<String> userIDArray = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userIDArray.add((String) userID);
        }

        zim.muteGroupMembers(
            isMute,
            userIDArray,
            groupID,
            configObj,
            new ZIMGroupMembersMutedCallback() {
                @Override
                public void onGroupMembersMuted(
                    String groupID,
                    boolean isMute,
                    int duration,
                    ArrayList<String> mutedMemberIDs,
                    ArrayList<ZIMErrorUserInfo> errorUserList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putBoolean("isMute", isMute);
                        returnMap.putInt("duration", duration);

                        WritableArray mutedUserIDs = Arguments.createArray();
                        for (String key : mutedMemberIDs) {
                            mutedUserIDs.pushString(key);
                        }
                        returnMap.putArray("mutedUserIDs", mutedUserIDs);

                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryGroupInfo(String handle, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.queryGroupInfo(
            groupID,
            new ZIMGroupInfoQueriedCallback() {
                @Override
                public void onGroupInfoQueried(ZIMGroupFullInfo groupInfo, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("groupInfo", RNZIMConverter.convertGroupFullInfo(groupInfo));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryGroupMemberCount(String handle, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.queryGroupMemberCount(
            groupID,
            new ZIMGroupMemberCountQueriedCallback() {
                @Override
                public void onGroupMemberCountQueried(String groupID, int count, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putInt("count", count);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void setGroupAttributes(String handle, ReadableMap groupAttributes, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        HashMap<String, String> groupAttrMap = new HashMap<>();
        for (String key : groupAttributes.toHashMap().keySet()) {
            groupAttrMap.put(key, groupAttributes.getString(key));
        }

        zim.setGroupAttributes(
            groupAttrMap,
            groupID,
            new ZIMGroupAttributesOperatedCallback() {
                @Override
                public void onGroupAttributesOperated(String groupID, ArrayList<String> errorKeys, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        WritableArray errorKeysArray = Arguments.createArray();
                        for (String key : errorKeys) {
                            errorKeysArray.pushString(key);
                        }
                        returnMap.putArray("errorKeys", errorKeysArray);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void deleteGroupAttributes(String handle, ReadableArray keys, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> keyArray = new ArrayList<>();
        for (Object key : keys.toArrayList()) {
            keyArray.add((String) key);
        }

        zim.deleteGroupAttributes(
            keyArray,
            groupID,
            new ZIMGroupAttributesOperatedCallback() {
                @Override
                public void onGroupAttributesOperated(String groupID, ArrayList<String> errorKeys, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        WritableArray errorKeysArray = Arguments.createArray();
                        for (String key : errorKeys) {
                            errorKeysArray.pushString(key);
                        }
                        returnMap.putArray("errorKeys", errorKeysArray);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryGroupAttributes(String handle, ReadableArray keys, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> keyArray = new ArrayList<>();
        for (Object key : keys.toArrayList()) {
            keyArray.add((String) key);
        }

        zim.queryGroupAttributes(
            keyArray,
            groupID,
            new ZIMGroupAttributesQueriedCallback() {
                @Override
                public void onGroupAttributesQueried(
                    String groupID,
                    HashMap<String, String> groupAttributes,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        WritableMap groupAttrMap = Arguments.createMap();
                        for (String key : groupAttributes.keySet()) {
                            groupAttrMap.putString(key, groupAttributes.get(key));
                        }
                        returnMap.putMap("groupAttributes", groupAttrMap);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryGroupAllAttributes(String handle, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.queryGroupAllAttributes(
            groupID,
            new ZIMGroupAttributesQueriedCallback() {
                @Override
                public void onGroupAttributesQueried(
                    String groupID,
                    HashMap<String, String> groupAttributes,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        WritableMap groupAttrMap = Arguments.createMap();
                        for (String key : groupAttributes.keySet()) {
                            groupAttrMap.putString(key, groupAttributes.get(key));
                        }
                        returnMap.putMap("groupAttributes", groupAttrMap);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateGroupAlias(String handle, String groupAlias, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateGroupAlias(
            groupAlias,
            groupID,
            new ZIMGroupAliasUpdatedCallback() {
                @Override
                public void onGroupAliasUpdated(String groupID, String groupAlias, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putString("groupAlias", groupAlias);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void setGroupMemberNickname(
        String handle,
        String nickname,
        String forUserID,
        String groupID,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.setGroupMemberNickname(
            nickname,
            forUserID,
            groupID,
            new ZIMGroupMemberNicknameUpdatedCallback() {
                @Override
                public void onGroupMemberNicknameUpdated(
                    String groupID,
                    String forUserID,
                    String nickname,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putString("forUserID", forUserID);
                        returnMap.putString("nickname", nickname);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void setGroupMemberRole(String handle, int role, String forUserID, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.setGroupMemberRole(
            role,
            forUserID,
            groupID,
            new ZIMGroupMemberRoleUpdatedCallback() {
                @Override
                public void onGroupMemberRoleUpdated(String groupID, String forUserID, int role, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putString("forUserID", forUserID);
                        returnMap.putInt("role", role);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void transferGroupOwner(String handle, String toUserID, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.transferGroupOwner(
            toUserID,
            groupID,
            new ZIMGroupOwnerTransferredCallback() {
                @Override
                public void onGroupOwnerTransferred(String groupID, String toUserID, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putString("toUserID", toUserID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryGroupMemberInfo(String handle, String userID, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.queryGroupMemberInfo(
            userID,
            groupID,
            new ZIMGroupMemberInfoQueriedCallback() {
                @Override
                public void onGroupMemberInfoQueried(String groupID, ZIMGroupMemberInfo userInfo, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putMap("userInfo", RNZIMConverter.convertGroupMemberInfo(userInfo));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void inviteUsersIntoGroup(String handle, ReadableArray userIDs, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userArray = new ArrayList<>();
        for (Object key : userIDs.toArrayList()) {
            userArray.add((String) key);
        }

        zim.inviteUsersIntoGroup(
            userArray,
            groupID,
            new ZIMGroupUsersInvitedCallback() {
                @Override
                public void onGroupUsersInvited(
                    String groupID,
                    ArrayList<ZIMGroupMemberInfo> userList,
                    ArrayList<ZIMErrorUserInfo> errorUserList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putArray("userList", RNZIMConverter.convertGroupMemberInfoList(userList));
                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void kickGroupMembers(String handle, ReadableArray userIDs, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userArray = new ArrayList<>();
        for (Object key : userIDs.toArrayList()) {
            userArray.add((String) key);
        }

        zim.kickGroupMembers(
            userArray,
            groupID,
            new ZIMGroupMemberKickedCallback() {
                @Override
                public void onGroupMemberKicked(
                    String groupID,
                    ArrayList<String> kickedUserIDList,
                    ArrayList<ZIMErrorUserInfo> errorUserList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        WritableArray userIDArray = Arguments.createArray();
                        for (String userID : kickedUserIDList) {
                            userIDArray.pushString(userID);
                        }
                        returnMap.putArray("kickedUserIDs", userIDArray);
                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryGroupMemberList(String handle, String groupID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupMemberQueryConfig configObj = new ZIMGroupMemberQueryConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextFlag = config.hasKey("nextFlag") ? config.getInt("nextFlag") : 0;
        }

        zim.queryGroupMemberList(
            groupID,
            configObj,
            new ZIMGroupMemberListQueriedCallback() {
                @Override
                public void onGroupMemberListQueried(
                    String groupID,
                    ArrayList<ZIMGroupMemberInfo> userList,
                    int nextFlag,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putArray("userList", RNZIMConverter.convertGroupMemberInfoList(userList));
                        returnMap.putInt("nextFlag", nextFlag);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryGroupMemberMutedList(String handle, String groupID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupMemberMutedListQueryConfig configObj = new ZIMGroupMemberMutedListQueryConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextFlag =
                config.hasKey("nextFlag") ? Double.valueOf(config.getDouble("nextFlag")).longValue() : 0;
        }

        zim.queryGroupMemberMutedList(
            groupID,
            configObj,
            new ZIMGroupMemberMutedListQueriedCallback() {
                @Override
                public void onGroupMemberListQueried(
                    String groupID,
                    long nextFlag,
                    ArrayList<ZIMGroupMemberInfo> userList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putDouble("nextFlag", nextFlag);
                        returnMap.putArray("userList", RNZIMConverter.convertGroupMemberInfoList(userList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateGroupJoinMode(String handle, int mode, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateGroupJoinMode(
            ZIMGroupJoinMode.getZIMGroupJoinMode(mode),
            groupID,
            new ZIMGroupJoinModeUpdatedCallback() {
                @Override
                public void onGroupJoinModeUpdated(String groupID, ZIMGroupJoinMode mode, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putInt("mode", mode.value());
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateGroupInviteMode(String handle, int mode, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateGroupInviteMode(
            ZIMGroupInviteMode.getZIMGroupInviteMode(mode),
            groupID,
            new ZIMGroupInviteModeUpdatedCallback() {
                @Override
                public void onGroupInviteModeUpdated(String groupID, ZIMGroupInviteMode mode, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putInt("mode", mode.value());
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateGroupBeInviteMode(String handle, int mode, String groupID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateGroupBeInviteMode(
            ZIMGroupBeInviteMode.getZIMGroupBeInviteMode(mode),
            groupID,
            new ZIMGroupBeInviteModeUpdatedCallback() {
                @Override
                public void onGroupBeInviteModeUpdated(String groupID, ZIMGroupBeInviteMode mode, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putInt("mode", mode.value());
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void sendGroupJoinApplication(String handle, String groupID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupJoinApplicationSendConfig configObj = new ZIMGroupJoinApplicationSendConfig();
        configObj.wording = config.hasKey("wording") ? config.getString("wording") : "";
        configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);

        zim.sendGroupJoinApplication(
            groupID,
            configObj,
            new ZIMGroupJoinApplicationSentCallback() {
                @Override
                public void onGroupJoinApplicationSent(String groupID, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void acceptGroupJoinApplication(
        String handle,
        String userID,
        String groupID,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupJoinApplicationAcceptConfig configObj = new ZIMGroupJoinApplicationAcceptConfig();
        configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);

        zim.acceptGroupJoinApplication(
            userID,
            groupID,
            configObj,
            new ZIMGroupJoinApplicationAcceptedCallback() {
                @Override
                public void onGroupJoinApplicationAccepted(String groupID, String userID, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putString("userID", userID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void rejectGroupJoinApplication(
        String handle,
        String userID,
        String groupID,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupJoinApplicationRejectConfig configObj = new ZIMGroupJoinApplicationRejectConfig();
        configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);

        zim.rejectGroupJoinApplication(
            userID,
            groupID,
            configObj,
            new ZIMGroupJoinApplicationRejectedCallback() {
                @Override
                public void onGroupJoinApplicationRejected(String groupID, String userID, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putString("userID", userID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void sendGroupInviteApplications(
        String handle,
        ReadableArray invitees,
        String groupID,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userArray = new ArrayList<>();
        for (Object key : invitees.toArrayList()) {
            userArray.add((String) key);
        }

        ZIMGroupInviteApplicationSendConfig configObj = new ZIMGroupInviteApplicationSendConfig();
        configObj.wording = config.hasKey("wording") ? config.getString("wording") : "";
        configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);

        zim.sendGroupInviteApplications(
            userArray,
            groupID,
            configObj,
            new ZIMGroupInviteApplicationsSentCallback() {
                @Override
                public void onGroupInviteApplicationsSent(
                    String groupID,
                    ArrayList<ZIMErrorUserInfo> errorUserList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void acceptGroupInviteApplication(
        String handle,
        String userID,
        String groupID,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupInviteApplicationAcceptConfig configObj = new ZIMGroupInviteApplicationAcceptConfig();
        configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);

        zim.acceptGroupInviteApplication(
            userID,
            groupID,
            configObj,
            new ZIMGroupInviteApplicationAcceptedCallback() {
                @Override
                public void onGroupInviteApplicationAccepted(
                    ZIMGroupFullInfo groupInfo,
                    String userID,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("groupInfo", RNZIMConverter.convertGroupFullInfo(groupInfo));
                        returnMap.putString("inviterUserID", userID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void rejectGroupInviteApplication(
        String handle,
        String userID,
        String groupID,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupInviteApplicationRejectConfig configObj = new ZIMGroupInviteApplicationRejectConfig();
        configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);

        zim.rejectGroupInviteApplication(
            userID,
            groupID,
            configObj,
            new ZIMGroupInviteApplicationRejectedCallback() {
                @Override
                public void onGroupInviteApplicationRejected(String groupID, String userID, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putString("inviterUserID", userID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryGroupApplicationList(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupApplicationListQueryConfig configObj = new ZIMGroupApplicationListQueryConfig();
        configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
        configObj.nextFlag = config.hasKey("nextFlag") ? config.getInt("nextFlag") : 0;

        zim.queryGroupApplicationList(
            configObj,
            new ZIMGroupApplicationListQueriedCallback() {
                @Override
                public void onGroupApplicationListQueried(
                    ArrayList<ZIMGroupApplicationInfo> applicationList,
                    int nextFlag,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putArray(
                            "applicationList",
                            RNZIMConverter.convertGroupApplicationList(applicationList)
                        );
                        returnMap.putInt("nextFlag", nextFlag);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void callInvite(String handle, ReadableArray invitees, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userArray = new ArrayList<>();
        for (Object key : invitees.toArrayList()) {
            userArray.add((String) key);
        }

        ZIMCallInviteConfig configObj = new ZIMCallInviteConfig();
        if (config != null) {
            configObj.mode =
                ZIMCallInvitationMode.getZIMCallInvitationMode(config.hasKey("mode") ? config.getInt("mode") : 0);
            configObj.timeout = config.hasKey("timeout") ? config.getInt("timeout") : 90;
            configObj.extendedData = config.hasKey("extendedData") ? config.getString("extendedData") : "";
            configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);
            configObj.enableNotReceivedCheck =
                config.hasKey("enableNotReceivedCheck") ? config.getBoolean("enableNotReceivedCheck") : false;
        }

        zim.callInvite(
            userArray,
            configObj,
            new ZIMCallInvitationSentCallback() {
                @Override
                public void onCallInvitationSent(String callID, ZIMCallInvitationSentInfo info, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("callID", callID);
                        returnMap.putInt("timeout", info.timeout);
                        returnMap.putArray(
                            "errorUserList",
                            RNZIMConverter.convertErrorUserInfoList(info.errorUserList)
                        );
                        returnMap.putArray("errorInvitees", RNZIMConverter.convertCallUserList(info.errorInvitees));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void callCancel(
        String handle,
        ReadableArray invitees,
        String callID,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userArray = new ArrayList<>();
        for (Object key : invitees.toArrayList()) {
            userArray.add((String) key);
        }

        ZIMCallCancelConfig configObj = new ZIMCallCancelConfig();
        if (config != null) {
            configObj.extendedData = config.hasKey("extendedData") ? config.getString("extendedData") : "";
            configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);
        }

        zim.callCancel(
            userArray,
            callID,
            configObj,
            new ZIMCallCancelSentCallback() {
                @Override
                public void onCallCancelSent(String callID, ArrayList<String> errorInvitees, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("callID", callID);
                        WritableArray errorUserArray = Arguments.createArray();
                        for (String invitee : errorInvitees) {
                            errorUserArray.pushString(invitee);
                        }
                        returnMap.putArray("errorInvitees", errorUserArray);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void callAccept(String handle, String callID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMCallAcceptConfig configObj = new ZIMCallAcceptConfig();
        if (config != null) {
            configObj.extendedData = config.hasKey("extendedData") ? config.getString("extendedData") : "";
        }

        zim.callAccept(
            callID,
            configObj,
            new ZIMCallAcceptanceSentCallback() {
                @Override
                public void onCallAcceptanceSent(String callID, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("callID", callID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void callReject(String handle, String callID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMCallRejectConfig configObj = new ZIMCallRejectConfig();
        if (config != null) {
            configObj.extendedData = config.hasKey("extendedData") ? config.getString("extendedData") : "";
        }

        zim.callReject(
            callID,
            configObj,
            new ZIMCallRejectionSentCallback() {
                @Override
                public void onCallRejectionSent(String callID, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("callID", callID);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void callQuit(String handle, String callID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMCallQuitConfig configObj = new ZIMCallQuitConfig();
        if (config != null) {
            configObj.extendedData = config.hasKey("extendedData") ? config.getString("extendedData") : "";
            configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);
        }

        zim.callQuit(
            callID,
            configObj,
            new ZIMCallQuitSentCallback() {
                @Override
                public void onCallQuitSent(String callID, ZIMCallQuitSentInfo info, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("callID", callID);
                        returnMap.putDouble("createTime", info.createTime);
                        returnMap.putDouble("acceptTime", info.acceptTime);
                        returnMap.putDouble("quitTime", info.quitTime);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void callEnd(String handle, String callID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMCallEndConfig configObj = new ZIMCallEndConfig();
        if (config != null) {
            configObj.extendedData = config.hasKey("extendedData") ? config.getString("extendedData") : "";
            configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);
        }

        zim.callEnd(
            callID,
            configObj,
            new ZIMCallEndSentCallback() {
                @Override
                public void onCallEndSent(String callID, ZIMCallEndedSentInfo info, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("callID", callID);
                        returnMap.putDouble("createTime", info.createTime);
                        returnMap.putDouble("acceptTime", info.acceptTime);
                        returnMap.putDouble("endTime", info.endTime);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void callJoin(String handle, String callID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMCallJoinConfig configObj = new ZIMCallJoinConfig();
        if (config != null) {
            configObj.extendedData = config.hasKey("extendedData") ? config.getString("extendedData") : "";
        }

        zim.callJoin(
            callID,
            configObj,
            new ZIMCallJoinSentCallback() {
                @Override
                public void onCallJoinSent(String callID, ZIMCallJoinSentInfo info, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("callID", callID);
                        returnMap.putDouble("createTime", info.createTime);
                        returnMap.putDouble("joinTime", info.joinTime);
                        returnMap.putString("extendedData", info.extendedData);
                        returnMap.putArray("callUserList", RNZIMConverter.convertCallUserList(info.callUserList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void callingInvite(
        String handle,
        ReadableArray invitees,
        String callID,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userArray = new ArrayList<>();
        for (Object key : invitees.toArrayList()) {
            userArray.add((String) key);
        }

        ZIMCallingInviteConfig configObj = new ZIMCallingInviteConfig();
        if (config != null) {
            configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);
        }

        zim.callingInvite(
            userArray,
            callID,
            configObj,
            new ZIMCallingInvitationSentCallback() {
                @Override
                public void onCallingInvitationSent(
                    String callID,
                    ZIMCallingInvitationSentInfo info,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("callID", callID);
                        returnMap.putArray(
                            "errorUserList",
                            RNZIMConverter.convertErrorUserInfoList(info.errorUserList)
                        );

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryCallInvitationList(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMCallInvitationQueryConfig configObj = new ZIMCallInvitationQueryConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextFlag =
                config.hasKey("nextFlag") ? Double.valueOf(config.getDouble("nextFlag")).longValue() : 0;
        }

        zim.queryCallInvitationList(
            configObj,
            new ZIMCallInvitationListQueriedCallback() {
                @Override
                public void onCallInvitationListQueried(
                    ArrayList<ZIMCallInfo> callList,
                    long nextFlag,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putDouble("nextFlag", nextFlag);
                        WritableArray callArray = Arguments.createArray();
                        for (ZIMCallInfo info : callList) {
                            WritableMap infoMap = Arguments.createMap();
                            infoMap.putInt("mode", info.mode.value());
                            infoMap.putInt("state", info.state.value());
                            infoMap.putString("callID", info.callID);
                            infoMap.putString("caller", info.caller);
                            infoMap.putString("extendedData", info.extendedData);
                            infoMap.putDouble("createTime", info.createTime);
                            infoMap.putDouble("endTime", info.endTime);
                            infoMap.putArray("callUserList", RNZIMConverter.convertCallUserList(info.callUserList));

                            callArray.pushMap(infoMap);
                        }
                        returnMap.putArray("callList", callArray);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void searchConversations(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMConversationSearchConfig configObj = new ZIMConversationSearchConfig();
        if (config != null) {
            configObj.nextFlag = config.hasKey("nextFlag") ? config.getInt("nextFlag") : 0;
            configObj.totalConversationCount =
                config.hasKey("totalConversationCount") ? config.getInt("totalConversationCount") : 0;
            configObj.conversationMessageCount =
                config.hasKey("conversationMessageCount") ? config.getInt("conversationMessageCount") : 0;
            configObj.startTime =
                config.hasKey("startTime") ? Double.valueOf(config.getDouble("startTime")).longValue() : 0;
            configObj.endTime = config.hasKey("endTime") ? Double.valueOf(config.getDouble("endTime")).longValue() : 0;
            if (config.hasKey("keywords")) {
                ArrayList<String> keywords = new ArrayList<>();
                for (Object key : config.getArray("keywords").toArrayList()) {
                    keywords.add((String) key);
                }
                configObj.keywords = keywords;
            }
            if (config.hasKey("senderUserIDs")) {
                ArrayList<String> senderUserIDs = new ArrayList<>();
                for (Object key : config.getArray("senderUserIDs").toArrayList()) {
                    senderUserIDs.add((String) key);
                }
                configObj.senderUserIDs = senderUserIDs;
            }
            if (config.hasKey("messageTypes")) {
                ArrayList<ZIMMessageType> messageTypes = new ArrayList<>();
                for (Object key : config.getArray("messageTypes").toArrayList()) {
                    messageTypes.add(ZIMMessageType.getZIMMessageType(Double.valueOf((double) key).intValue()));
                }
                configObj.messageTypes = messageTypes;
            }
            if (config.hasKey("subMessageTypes")) {
                ArrayList<Integer> subMessageTypes = new ArrayList<>();
                for (Object key : config.getArray("subMessageTypes").toArrayList()) {
                    subMessageTypes.add(Double.valueOf((double) key).intValue());
                }
                configObj.subMessageTypes = subMessageTypes;
            }
        }

        zim.searchLocalConversations(
            configObj,
            new ZIMConversationsSearchedCallback() {
                @Override
                public void onConversationsSearched(
                    ArrayList<ZIMConversationSearchInfo> conversationSearchInfoList,
                    int nextFlag,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putInt("nextFlag", nextFlag);

                        WritableArray infoArray = Arguments.createArray();
                        for (ZIMConversationSearchInfo info : conversationSearchInfoList) {
                            WritableMap infoMap = Arguments.createMap();
                            infoMap.putString("conversationID", info.conversationID);
                            infoMap.putInt("conversationType", info.conversationType.value());
                            infoMap.putInt("totalMessageCount", info.totalMessageCount);
                            infoMap.putArray("messageList", RNZIMConverter.convertMessageList(info.messageList));

                            infoArray.pushMap(infoMap);
                        }
                        returnMap.putArray("conversationSearchInfoList", infoArray);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void searchGlobalMessages(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessageSearchConfig configObj = new ZIMMessageSearchConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.order = ZIMMessageOrder.getZIMMessageOrder(config.hasKey("order") ? config.getInt("order") : 0);
            configObj.startTime =
                config.hasKey("startTime") ? Double.valueOf(config.getDouble("startTime")).longValue() : 0;
            configObj.endTime = config.hasKey("endTime") ? Double.valueOf(config.getDouble("endTime")).longValue() : 0;
            if (config.hasKey("nextMessage")) {
                configObj.nextMessage = RNZIMConverter.convertMessageFromJsToJava(config.getMap("nextMessage"));
            }
            if (config.hasKey("keywords")) {
                ArrayList<String> keywords = new ArrayList<>();
                for (Object key : config.getArray("keywords").toArrayList()) {
                    keywords.add((String) key);
                }
                configObj.keywords = keywords;
            }
            if (config.hasKey("senderUserIDs")) {
                ArrayList<String> senderUserIDs = new ArrayList<>();
                for (Object key : config.getArray("senderUserIDs").toArrayList()) {
                    senderUserIDs.add((String) key);
                }
                configObj.senderUserIDs = senderUserIDs;
            }
            if (config.hasKey("messageTypes")) {
                ArrayList<ZIMMessageType> messageTypes = new ArrayList<>();
                for (Object key : config.getArray("messageTypes").toArrayList()) {
                    messageTypes.add(ZIMMessageType.getZIMMessageType(Double.valueOf((double) key).intValue()));
                }
                configObj.messageTypes = messageTypes;
            }
            if (config.hasKey("subMessageTypes")) {
                ArrayList<Integer> subMessageTypes = new ArrayList<>();
                for (Object key : config.getArray("subMessageTypes").toArrayList()) {
                    subMessageTypes.add(Double.valueOf((double) key).intValue());
                }
                configObj.subMessageTypes = subMessageTypes;
            }
        }

        zim.searchGlobalLocalMessages(
            configObj,
            new ZIMMessagesGlobalSearchedCallback() {
                @Override
                public void onMessagesGlobalSearched(
                    ArrayList<ZIMMessage> messageList,
                    ZIMMessage nextMessage,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        if (nextMessage != null) {
                            returnMap.putMap("nextMessage", RNZIMConverter.convertMessage(nextMessage));
                        } else {
                            returnMap.putNull("nextMessage");
                        }

                        returnMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void searchMessages(
        String handle,
        String conversationID,
        int conversationType,
        ReadableMap config,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMMessageSearchConfig configObj = new ZIMMessageSearchConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.order = ZIMMessageOrder.getZIMMessageOrder(config.hasKey("order") ? config.getInt("order") : 0);
            configObj.startTime =
                config.hasKey("startTime") ? Double.valueOf(config.getDouble("startTime")).longValue() : 0;
            configObj.endTime = config.hasKey("endTime") ? Double.valueOf(config.getDouble("endTime")).longValue() : 0;
            if (config.hasKey("nextMessage")) {
                configObj.nextMessage = RNZIMConverter.convertMessageFromJsToJava(config.getMap("nextMessage"));
            }
            if (config.hasKey("keywords")) {
                ArrayList<String> keywords = new ArrayList<>();
                for (Object key : config.getArray("keywords").toArrayList()) {
                    keywords.add((String) key);
                }
                configObj.keywords = keywords;
            }
            if (config.hasKey("senderUserIDs")) {
                ArrayList<String> senderUserIDs = new ArrayList<>();
                for (Object key : config.getArray("senderUserIDs").toArrayList()) {
                    senderUserIDs.add((String) key);
                }
                configObj.senderUserIDs = senderUserIDs;
            }
            if (config.hasKey("messageTypes")) {
                ArrayList<ZIMMessageType> messageTypes = new ArrayList<>();
                for (Object key : config.getArray("messageTypes").toArrayList()) {
                    messageTypes.add(ZIMMessageType.getZIMMessageType(Double.valueOf((double) key).intValue()));
                }
                configObj.messageTypes = messageTypes;
            }
            if (config.hasKey("subMessageTypes")) {
                ArrayList<Integer> subMessageTypes = new ArrayList<>();
                for (Object key : config.getArray("subMessageTypes").toArrayList()) {
                    subMessageTypes.add(Double.valueOf((double) key).intValue());
                }
                configObj.subMessageTypes = subMessageTypes;
            }
        }

        zim.searchLocalMessages(
            conversationID,
            ZIMConversationType.getZIMConversationType(conversationType),
            configObj,
            new ZIMMessagesSearchedCallback() {
                @Override
                public void onMessagesSearched(
                    String conversationID,
                    ZIMConversationType conversationType,
                    ArrayList<ZIMMessage> messageList,
                    ZIMMessage nextMessage,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("conversationID", conversationID);
                        returnMap.putInt("conversationType", conversationType.value());

                        if (nextMessage != null) {
                            returnMap.putMap("nextMessage", RNZIMConverter.convertMessage(nextMessage));
                        } else {
                            returnMap.putNull("nextMessage");
                        }

                        returnMap.putArray("messageList", RNZIMConverter.convertMessageList(messageList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void searchGroups(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupSearchConfig configObj = new ZIMGroupSearchConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextFlag = config.hasKey("nextFlag") ? config.getInt("nextFlag") : 0;
            configObj.isAlsoMatchGroupMemberUserName =
                config.hasKey("isAlsoMatchGroupMemberUserName")
                    ? config.getBoolean("isAlsoMatchGroupMemberUserName")
                    : false;
            configObj.isAlsoMatchGroupMemberNickname =
                config.hasKey("isAlsoMatchGroupMemberNickname")
                    ? config.getBoolean("isAlsoMatchGroupMemberNickname")
                    : false;
            if (config.hasKey("keywords")) {
                ArrayList<String> keywords = new ArrayList<>();
                for (Object key : config.getArray("keywords").toArrayList()) {
                    keywords.add((String) key);
                }
                configObj.keywords = keywords;
            }
        }

        zim.searchLocalGroups(
            configObj,
            new ZIMGroupsSearchedCallback() {
                @Override
                public void onGroupsSearched(
                    ArrayList<ZIMGroupSearchInfo> groupSearchInfoList,
                    int nextFlag,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putInt("nextFlag", nextFlag);

                        WritableArray infoArray = Arguments.createArray();
                        for (ZIMGroupSearchInfo info : groupSearchInfoList) {
                            WritableMap infoMap = Arguments.createMap();
                            infoMap.putMap("groupInfo", RNZIMConverter.convertGroupInfo(info.groupInfo));
                            infoMap.putArray("userList", RNZIMConverter.convertGroupMemberInfoList(info.userList));
                            infoArray.pushMap(infoMap);
                        }
                        returnMap.putArray("groupSearchInfoList", infoArray);

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void searchGroupMembers(String handle, String groupID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMGroupMemberSearchConfig configObj = new ZIMGroupMemberSearchConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextFlag = config.hasKey("nextFlag") ? config.getInt("nextFlag") : 0;
            configObj.isAlsoMatchGroupMemberNickname =
                config.hasKey("isAlsoMatchGroupMemberNickname")
                    ? config.getBoolean("isAlsoMatchGroupMemberNickname")
                    : false;
            if (config.hasKey("keywords")) {
                ArrayList<String> keywords = new ArrayList<>();
                for (Object key : config.getArray("keywords").toArrayList()) {
                    keywords.add((String) key);
                }
                configObj.keywords = keywords;
            }
        }

        zim.searchLocalGroupMembers(
            groupID,
            configObj,
            new ZIMGroupMembersSearchedCallback() {
                @Override
                public void onGroupMembersSearched(
                    String groupID,
                    ArrayList<ZIMGroupMemberInfo> userList,
                    int nextFlag,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putString("groupID", groupID);
                        returnMap.putInt("nextFlag", nextFlag);
                        returnMap.putArray("userList", RNZIMConverter.convertGroupMemberInfoList(userList));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void searchFriends(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMFriendSearchConfig configObj = new ZIMFriendSearchConfig();
        if (config != null) {
            configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
            configObj.nextFlag = config.hasKey("nextFlag") ? config.getInt("nextFlag") : 0;
            configObj.isAlsoMatchFriendAlias =
                config.hasKey("isAlsoMatchFriendAlias") ? config.getBoolean("isAlsoMatchFriendAlias") : false;
            if (config.hasKey("keywords")) {
                ArrayList<String> keywords = new ArrayList<>();
                for (Object key : config.getArray("keywords").toArrayList()) {
                    keywords.add((String) key);
                }
                configObj.keywords = keywords;
            }
        }

        zim.searchLocalFriends(
            configObj,
            new ZIMFriendsSearchedCallback() {
                @Override
                public void onFriendsSearched(ArrayList<ZIMFriendInfo> friendInfos, int nextFlag, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putInt("nextFlag", nextFlag);
                        returnMap.putArray("friendInfos", RNZIMConverter.convertFriendInfoList(friendInfos));

                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void addFriend(String handle, String userID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMFriendAddConfig configObj = new ZIMFriendAddConfig();
        configObj.wording = config.hasKey("wording") ? config.getString("wording") : "";
        configObj.friendAlias = config.hasKey("friendAlias") ? config.getString("friendAlias") : "";
        if (config.hasKey("friendAttributes")) {
            HashMap<String, String> attributes = new HashMap<>();
            HashMap<String, Object> map = config.getMap("friendAttributes").toHashMap();
            for (String key : map.keySet()) {
                attributes.put(key, (String) map.get(key));
            }
            configObj.friendAttributes = attributes;
        }

        zim.addFriend(
            userID,
            configObj,
            new ZIMFriendAddedCallback() {
                @Override
                public void onFriendAddedCallback(ZIMFriendInfo friendInfo, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("friendInfo", RNZIMConverter.convertFriendInfo(friendInfo));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void sendFriendApplication(String handle, String userID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMFriendApplicationSendConfig configObj = new ZIMFriendApplicationSendConfig();
        configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);
        configObj.wording = config.hasKey("wording") ? config.getString("wording") : "";
        configObj.friendAlias = config.hasKey("friendAlias") ? config.getString("friendAlias") : "";
        if (config.hasKey("friendAttributes")) {
            HashMap<String, String> attributes = new HashMap<>();
            HashMap<String, Object> map = config.getMap("friendAttributes").toHashMap();
            for (String key : map.keySet()) {
                attributes.put(key, (String) map.get(key));
            }
            configObj.friendAttributes = attributes;
        }

        zim.sendFriendApplication(
            userID,
            configObj,
            new ZIMFriendApplicationSentCallback() {
                @Override
                public void onFriendApplicationSentCallback(
                    ZIMFriendApplicationInfo applicationInfo,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("applicationInfo", RNZIMConverter.convertFriendApplication(applicationInfo));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void deleteFriends(String handle, ReadableArray userIDs, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMFriendDeleteConfig configObj = new ZIMFriendDeleteConfig();
        configObj.type = ZIMFriendDeleteType.getZIMFriendDeleteType(config.hasKey("type") ? config.getInt("type") : 0);

        ArrayList<String> userIDArray = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userIDArray.add((String) userID);
        }

        zim.deleteFriends(
            userIDArray,
            configObj,
            new ZIMFriendsDeletedCallback() {
                @Override
                public void onFriendsDeletedCallback(ArrayList<ZIMErrorUserInfo> errorUserList, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void checkFriendsRelation(String handle, ReadableArray userIDs, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMFriendRelationCheckConfig configObj = new ZIMFriendRelationCheckConfig();
        configObj.type =
            ZIMFriendRelationCheckType.getZIMFriendRelationCheckType(config.hasKey("type") ? config.getInt("type") : 0);

        ArrayList<String> userIDArray = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userIDArray.add((String) userID);
        }

        zim.checkFriendsRelation(
            userIDArray,
            configObj,
            new ZIMFriendsRelationCheckedCallback() {
                @Override
                public void onFriendsChecked(
                    ArrayList<ZIMFriendRelationInfo> relationInfos,
                    ArrayList<ZIMErrorUserInfo> errorUserList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();

                        WritableArray relationInfoArray = Arguments.createArray();
                        for (ZIMFriendRelationInfo info : relationInfos) {
                            WritableMap map = Arguments.createMap();
                            map.putString("userID", info.userID);
                            map.putInt("type", info.type.value());
                            relationInfoArray.pushMap(map);
                        }
                        returnMap.putArray("relationInfos", relationInfoArray);

                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateFriendAlias(String handle, String friendAlias, String userID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.updateFriendAlias(
            friendAlias,
            userID,
            new ZIMFriendAliasUpdatedCallback() {
                @Override
                public void onFriendAliasUpdated(ZIMFriendInfo friendInfo, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("friendInfo", RNZIMConverter.convertFriendInfo(friendInfo));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void updateFriendAttributes(
        String handle,
        ReadableMap friendAttributes,
        String userID,
        final Promise promise
    ) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        HashMap<String, String> attrMap = new HashMap<>();
        for (String key : friendAttributes.toHashMap().keySet()) {
            attrMap.put(key, friendAttributes.getString(key));
        }

        zim.updateFriendAttributes(
            attrMap,
            userID,
            new ZIMFriendAttributesUpdatedCallback() {
                @Override
                public void onFriendAttributesUpdated(ZIMFriendInfo friendInfo, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("friendInfo", RNZIMConverter.convertFriendInfo(friendInfo));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void acceptFriendApplication(String handle, String userID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMFriendApplicationAcceptConfig configObj = new ZIMFriendApplicationAcceptConfig();
        configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);
        configObj.friendAlias = config.hasKey("friendAlias") ? config.getString("friendAlias") : "";
        if (config.hasKey("friendAttributes")) {
            HashMap<String, String> attributes = new HashMap<>();
            HashMap<String, Object> map = config.getMap("friendAttributes").toHashMap();
            for (String key : map.keySet()) {
                attributes.put(key, (String) map.get(key));
            }
            configObj.friendAttributes = attributes;
        }

        zim.acceptFriendApplication(
            userID,
            configObj,
            new ZIMFriendApplicationAcceptedCallback() {
                @Override
                public void onFriendApplicationAccepted(ZIMFriendInfo friendInfo, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("friendInfo", RNZIMConverter.convertFriendInfo(friendInfo));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void rejectFriendApplication(String handle, String userID, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMFriendApplicationRejectConfig configObj = new ZIMFriendApplicationRejectConfig();
        configObj.pushConfig = RNZIMConverter.convertZIMPushConfig(config);

        zim.rejectFriendApplication(
            userID,
            configObj,
            new ZIMFriendApplicationRejectedCallback() {
                @Override
                public void onFriendApplicationRejected(ZIMUserInfo userInfo, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putMap("userInfo", RNZIMConverter.convertUserInfo(userInfo));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryFriendsInfo(String handle, ReadableArray userIDs, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userIDArray = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userIDArray.add((String) userID);
        }

        zim.queryFriendsInfo(
            userIDArray,
            new ZIMFriendsInfoQueriedCallback() {
                @Override
                public void onFriendsInfoQueried(
                    ArrayList<ZIMFriendInfo> friendInfos,
                    ArrayList<ZIMErrorUserInfo> errorUserList,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putArray("friendInfos", RNZIMConverter.convertFriendInfoList(friendInfos));
                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryFriendList(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMFriendListQueryConfig configObj = new ZIMFriendListQueryConfig();
        configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
        configObj.nextFlag = config.hasKey("nextFlag") ? config.getInt("nextFlag") : 0;

        zim.queryFriendList(
            configObj,
            new ZIMFriendListQueriedCallback() {
                @Override
                public void onFriendListQueried(ArrayList<ZIMFriendInfo> friendList, int nextFlag, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putInt("nextFlag", nextFlag);
                        returnMap.putArray("friendList", RNZIMConverter.convertFriendInfoList(friendList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryFriendApplicationList(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMFriendApplicationListQueryConfig configObj = new ZIMFriendApplicationListQueryConfig();
        configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
        configObj.nextFlag = config.hasKey("nextFlag") ? config.getInt("nextFlag") : 0;

        zim.queryFriendApplicationList(
            configObj,
            new ZIMFriendApplicationListQueriedCallback() {
                @Override
                public void onFriendApplicationListQueried(
                    ArrayList<ZIMFriendApplicationInfo> applicationList,
                    int nextFlag,
                    ZIMError errorInfo
                ) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putInt("nextFlag", nextFlag);
                        returnMap.putArray(
                            "applicationList",
                            RNZIMConverter.convertFriendApplicationList(applicationList)
                        );
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void addUsersToBlacklist(String handle, ReadableArray userIDs, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userIDArray = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userIDArray.add((String) userID);
        }

        zim.addUsersToBlacklist(
            userIDArray,
            new ZIMBlacklistUsersAddedCallback() {
                @Override
                public void onBlacklistUsersAdded(ArrayList<ZIMErrorUserInfo> errorUserList, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void removeUsersFromBlacklist(String handle, ReadableArray userIDs, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ArrayList<String> userIDArray = new ArrayList<>();
        for (Object userID : userIDs.toArrayList()) {
            userIDArray.add((String) userID);
        }

        zim.removeUsersFromBlacklist(
            userIDArray,
            new ZIMBlacklistUsersRemovedCallback() {
                @Override
                public void onBlacklistUsersRemoved(ArrayList<ZIMErrorUserInfo> errorUserList, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putArray("errorUserList", RNZIMConverter.convertErrorUserInfoList(errorUserList));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void checkUserIsInBlacklist(String handle, String userID, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        zim.checkUserIsInBlacklist(
            userID,
            new ZIMBlacklistCheckedCallback() {
                @Override
                public void onBlacklistChecked(boolean isUserInBlacklist, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putBoolean("isUserInBlacklist", isUserInBlacklist);
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }

    @ReactMethod
    public void queryBlacklist(String handle, ReadableMap config, final Promise promise) {
        ZIM zim = this.engineMap.get(handle);
        if (zim == null) {
            promise.reject("-1", "no instance");
            return;
        }

        ZIMBlacklistQueryConfig configObj = new ZIMBlacklistQueryConfig();
        configObj.count = config.hasKey("count") ? config.getInt("count") : 0;
        configObj.nextFlag = config.hasKey("nextFlag") ? config.getInt("nextFlag") : 0;

        zim.queryBlacklist(
            configObj,
            new ZIMBlacklistQueriedCallback() {
                @Override
                public void onBlacklistQueried(ArrayList<ZIMUserInfo> blacklist, int nextFlag, ZIMError errorInfo) {
                    if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                        WritableMap returnMap = Arguments.createMap();
                        returnMap.putInt("nextFlag", nextFlag);
                        returnMap.putArray("blacklist", RNZIMConverter.convertUserInfoList(blacklist));
                        promise.resolve(returnMap);
                    } else {
                        promise.reject(String.valueOf(errorInfo.code.value()), errorInfo.message);
                    }
                }
            }
        );
    }
}
