Kaynağa Gözat

update nuCore and fix bugs for release 5.0.27

alqindiirsyam 3 ay önce
ebeveyn
işleme
5275579038
29 değiştirilmiş dosya ile 1082 ekleme ve 823 silme
  1. 2 0
      AppBuilder/AppBuilder/AppBuilder.entitlements
  2. 4 2
      AppBuilder/AppBuilder/AppDelegate.swift
  3. 73 13
      AppBuilder/AppBuilder/FourthTabViewController.swift
  4. 1 0
      AppBuilder/AppBuilder/SceneDelegate.swift
  5. 1 1
      AppBuilder/AppBuilderShare/ShareViewController.swift
  6. 1 1
      NexilisLite/NexilisLite.podspec
  7. 144 45
      NexilisLite/NexilisLite/Source/APIS.swift
  8. 1 8
      NexilisLite/NexilisLite/Source/Callback.swift
  9. 0 9
      NexilisLite/NexilisLite/Source/CoreMessage_TMessageBank.swift
  10. 15 17
      NexilisLite/NexilisLite/Source/Extension.swift
  11. 68 36
      NexilisLite/NexilisLite/Source/IncomingThread.swift
  12. 1 0
      NexilisLite/NexilisLite/Source/Model/User.swift
  13. 29 40
      NexilisLite/NexilisLite/Source/Nexilis.swift
  14. 1 1
      NexilisLite/NexilisLite/Source/Utils.swift
  15. 9 9
      NexilisLite/NexilisLite/Source/View/Call/AudioViewController.swift
  16. 0 105
      NexilisLite/NexilisLite/Source/View/Call/Call.swift
  17. 11 0
      NexilisLite/NexilisLite/Source/View/Call/CallLogVC.swift
  18. 151 62
      NexilisLite/NexilisLite/Source/View/Call/CallManager.swift
  19. 0 199
      NexilisLite/NexilisLite/Source/View/Call/CallProviderDelegate.swift
  20. 1 1
      NexilisLite/NexilisLite/Source/View/Call/QmeraAudioConference.swift
  21. 153 86
      NexilisLite/NexilisLite/Source/View/Call/QmeraAudioViewController.swift
  22. 109 56
      NexilisLite/NexilisLite/Source/View/Call/QmeraVideoViewController.swift
  23. 127 42
      NexilisLite/NexilisLite/Source/View/Chat/ChatWALikeVC.swift
  24. 52 36
      NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift
  25. 52 38
      NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift
  26. 73 13
      NexilisLite/NexilisLite/Source/View/Control/SettingTableViewController.swift
  27. 1 1
      NexilisLite/Podfile
  28. 1 1
      StreamShield/Podfile
  29. 1 1
      StreamShield/StreamShield.podspec

+ 2 - 0
AppBuilder/AppBuilder/AppBuilder.entitlements

@@ -4,6 +4,8 @@
 <dict>
 	<key>aps-environment</key>
 	<string>development</string>
+	<key>com.apple.developer.calling-app</key>
+	<true/>
 	<key>com.apple.security.application-groups</key>
 	<array>
 		<string>group.nexilis.share</string>

+ 4 - 2
AppBuilder/AppBuilder/AppDelegate.swift

@@ -71,8 +71,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate {
     }
     
     func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
-        print("Received VoIP Push: \(payload.dictionaryPayload)")
-        completion()
+        let payload = payload.dictionaryPayload
+        APIS.showNotificationCallKitNexilis(payload: payload) {
+            completion()
+        }
     }
 
 

+ 73 - 13
AppBuilder/AppBuilder/FourthTabViewController.swift

@@ -206,6 +206,24 @@ public class FourthTabViewController: UIViewController, UITableViewDelegate, UIT
                                         dataImage["name"] = imageSignIn
                                         NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
                                     }
+                                } else if FileEncryption.shared.isSecureExists(filename: image!) {
+                                    do {
+                                        if var data = try FileEncryption.shared.readSecure(filename: image!) {
+                                            let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                            if dataDecrypt != nil {
+                                                data = dataDecrypt!
+                                            }
+                                            let image = UIImage(data: data)
+                                            Item.menus["Personal"]?[0].icon = image?.circleMasked
+                                            if !imageSignIn.isEmpty {
+                                                var dataImage: [AnyHashable : Any] = [:]
+                                                dataImage["name"] = imageSignIn
+                                                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                            }
+                                        }
+                                    } catch {
+                                        
+                                    }
                                 } else {
                                     Download().start(forKey: image!) { (name, progress) in
                                         guard progress == 100 else {
@@ -213,13 +231,24 @@ public class FourthTabViewController: UIViewController, UITableViewDelegate, UIT
                                         }
                                         
                                         DispatchQueue.main.async {
-                                            let image = UIImage(contentsOfFile: file.path)
-                                            Item.menus["Personal"]?[0].icon = image?.circleMasked
-                                            self.tableView.reloadData()
-                                            if !imageSignIn.isEmpty {
-                                                var dataImage: [AnyHashable : Any] = [:]
-                                                dataImage["name"] = imageSignIn
-                                                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                            if FileEncryption.shared.isSecureExists(filename: image!) {
+                                                do {
+                                                    if var data = try FileEncryption.shared.readSecure(filename: image!) {
+                                                        let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                                        if dataDecrypt != nil {
+                                                            data = dataDecrypt!
+                                                        }
+                                                        let image = UIImage(data: data)
+                                                        Item.menus["Personal"]?[0].icon = image?.circleMasked
+                                                        if !imageSignIn.isEmpty {
+                                                            var dataImage: [AnyHashable : Any] = [:]
+                                                            dataImage["name"] = imageSignIn
+                                                            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                                        }
+                                                    }
+                                                } catch {
+                                                    
+                                                }
                                             }
                                         }
                                     }
@@ -247,18 +276,49 @@ public class FourthTabViewController: UIViewController, UITableViewDelegate, UIT
                                 var dataImage: [AnyHashable : Any] = [:]
                                 dataImage["name"] = imageSignIn
                                 NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                            } else if FileEncryption.shared.isSecureExists(filename: imageSignIn) {
+                                do {
+                                    if var data = try FileEncryption.shared.readSecure(filename: imageSignIn) {
+                                        let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                        if dataDecrypt != nil {
+                                            data = dataDecrypt!
+                                        }
+                                        let image = UIImage(data: data)
+                                        Item.menus["Personal"]?[0].icon = image?.circleMasked
+                                        if !imageSignIn.isEmpty {
+                                            var dataImage: [AnyHashable : Any] = [:]
+                                            dataImage["name"] = imageSignIn
+                                            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                        }
+                                    }
+                                } catch {
+                                    
+                                }
                             } else {
                                 Download().start(forKey: imageSignIn) { (name, progress) in
                                     guard progress == 100 else {
                                         return
                                     }
                                     DispatchQueue.main.async {
-                                        let image = UIImage(contentsOfFile: file.path)
-                                        Item.menus["Personal"]?[0].icon = image?.circleMasked
-                                        self.tableView.reloadData()
-                                        var dataImage: [AnyHashable : Any] = [:]
-                                        dataImage["name"] = imageSignIn
-                                        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                        if FileEncryption.shared.isSecureExists(filename: imageSignIn) {
+                                            do {
+                                                if var data = try FileEncryption.shared.readSecure(filename: imageSignIn) {
+                                                    let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                                    if dataDecrypt != nil {
+                                                        data = dataDecrypt!
+                                                    }
+                                                    let image = UIImage(data: data)
+                                                    Item.menus["Personal"]?[0].icon = image?.circleMasked
+                                                    if !imageSignIn.isEmpty {
+                                                        var dataImage: [AnyHashable : Any] = [:]
+                                                        dataImage["name"] = imageSignIn
+                                                        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                                    }
+                                                }
+                                            } catch {
+                                                
+                                            }
+                                        }
                                     }
                                 }
                             }

+ 1 - 0
AppBuilder/AppBuilder/SceneDelegate.swift

@@ -93,6 +93,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
     }
 
     func sceneWillEnterForeground(_ scene: UIScene) {
+        APIS.willEnterForeground()
         // Called as the scene transitions from the background to the foreground.
         // Use this method to undo the changes made on entering the background.
     }

+ 1 - 1
AppBuilder/AppBuilderShare/ShareViewController.swift

@@ -474,7 +474,7 @@ class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDat
         
         textView = UITextView()
         viewVc.addSubview(textView)
-        textView.textColor = .white
+        textView.textColor = .label
         textView.font = .systemFont(ofSize: 17)
         textView.textContainerInset = UIEdgeInsets(top: 10.5, left: 15, bottom: 10.5, right: 15)
         textView.translatesAutoresizingMaskIntoConstraints = false

+ 1 - 1
NexilisLite/NexilisLite.podspec

@@ -24,7 +24,7 @@ Pod::Spec.new do |spec|
   spec.resource_bundles = { 'NexilisLite' => ['NexilisLite/Resource/**/*']}
   spec.swift_version = '5.5.1'
   spec.dependency 'FMDB', '~> 2.7.12'
-  spec.dependency 'nuSDKService', '4.0.19'
+  spec.dependency 'nuSDKService', '4.0.20'
   spec.dependency 'NotificationBannerSwift'
   spec.dependency 'Alamofire', '~> 5.10.2'
   spec.dependency 'SDWebImage', '~> 5.20.0'

+ 144 - 45
NexilisLite/NexilisLite/Source/APIS.swift

@@ -913,6 +913,7 @@ public class APIS: NSObject {
     public static func showNotificationNexilis(_ userInfo: [AnyHashable : Any]) {
         if checkAppStateisBackground() {
 //            Nexilis.sendStateToServer(s: "MASUK SHOW NOTIFICATION NEXILIS")
+            print("MASUK SHOW NOTIFICATION NEXILIS: \(userInfo)")
             DispatchQueue.main.async {
                 if let payload = userInfo["payload"] as? [String: Any] {
                     if let messagePayload = payload["message"] as? [String: Any] {
@@ -949,14 +950,6 @@ public class APIS: NSObject {
 //                                uuidCall = UUID()
                                 fpinCall = callFrom
                                 Nexilis.callAPNActivated = true
-    //                                    CallManager.shared.reportIncomingCall(uuid: uuidCall!, callerName: callFromName, callerId: callFrom, isVideo: callType != "1") { error in
-    //                                        if let error = error {
-    //                                            print("Error reporting incoming call: \(error.localizedDescription)")
-    //                                        } else {
-    //                                            print("Incoming call reported successfully")
-    //                                        }
-    //                                    }
-//                                copySoundToLocalPath("pb_call_in_ios", false)
                                 let center = UNUserNotificationCenter.current()
                                 let content = UNMutableNotificationContent()
                                 content.title = callFromName
@@ -967,8 +960,7 @@ public class APIS: NSObject {
                                 }
                                 content.userInfo = ["id" : callFrom, "type" : code, "callType": callType]
                                 content.sound = nil
-                                let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
-                                let request = UNNotificationRequest(identifier: callFrom, content: content, trigger: trigger)
+                                let request = UNNotificationRequest(identifier: callFrom, content: content, trigger: nil)
                                 center.add(request) { error in
                                     if let error = error {
                                         print("Error scheduling notification: \(error.localizedDescription)")
@@ -1003,19 +995,68 @@ public class APIS: NSObject {
                                 content.body = "☎️ Missed \(textCall) call".localized()
                                 content.userInfo = ["id" : callFrom, "type" : code, "callType": callType]
                                 content.sound = nil
-                                let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
-                                let request = UNNotificationRequest(identifier: callFrom, content: content, trigger: trigger)
+                                let request = UNNotificationRequest(identifier: callFrom, content: content, trigger: nil)
                                 center.add(request) { error in
                                     if let error = error {
                                         print("Error scheduling notification: \(error.localizedDescription)")
                                     }
                                 }
                                 Nexilis.saveMessageCall(idCall: (User.getMyPin() ?? "") + CoreMessage_TMessageUtil.getTID(), textMessage: "Missed \(textCall) call".localized() + " at 0", fPin: callFrom, lPin: (User.getMyPin() ?? ""), timeCall: String(Date().currentTimeMillis()), attachment_type: MessageScope.MISSED_CALL)
-    //                                    CallManager.shared.endCall(with: uuidCall)
-//                                }
                             }
                         }
                     }
+                } else if let message_id = userInfo["message_id"] as? String {
+                    getMessageById(id: message_id)
+                }
+            }
+        }
+    }
+    
+    public static func showNotificationCallKitNexilis(payload: [AnyHashable : Any], completion: @escaping () -> ()) {
+        if let messagePayload = payload["payload"] as? [String: Any] {
+            if let message = messagePayload["message"] as? [String: Any] {
+                if let data = message["data"] as? [String: Any] {
+                    let nxCode = data["nx_code"] as? String ?? ""
+                    let callFromName = data["call-from-name"] as? String ?? ""
+                    let callCancelName = data["call-cancel-name"] as? String ?? ""
+                    let callFrom = data["call-from"] as? String ?? ""
+                    let callCancel = data["call-cancel"] as? String ?? ""
+                    let callType = data["call-type"] as? String ?? ""
+                    if nxCode == "CL03" {
+                        Nexilis.callAPNActivated = true
+                        APIS.uuidCall = UUID()
+                        CallManager.shared.reportIncomingCall(uuid: APIS.uuidCall ?? UUID(), callerName: callFromName, callerId: callFrom, isVideo: callType != "1")
+                    } else {
+                        if APIS.uuidCall != nil {
+                            CallManager.shared.endCall(uuid: APIS.uuidCall!) {
+                                Nexilis.callAPNActivated = false
+                                APIS.uuidCall = nil
+                                let center = UNUserNotificationCenter.current()
+                                var textCall = ""
+                                if callType == "1" {
+                                    textCall = "audio"
+                                } else {
+                                    textCall = "video"
+                                }
+                                let content = UNMutableNotificationContent()
+                                content.title = callCancelName
+                                content.body = "☎️ Missed \(textCall) call".localized()
+                                content.userInfo = ["id" : callFrom, "type" : nxCode, "callType": callType]
+                                content.sound = nil
+                                let request = UNNotificationRequest(identifier: callCancel, content: content, trigger: nil)
+                                center.add(request) { error in
+                                    if let error = error {
+                                        print("Error scheduling notification: \(error.localizedDescription)")
+                                    }
+                                }
+                                Nexilis.saveMessageCall(idCall: (User.getMyPin() ?? "") + CoreMessage_TMessageUtil.getTID(), textMessage: "Missed \(textCall) call".localized() + " at 0", fPin: callCancel, lPin: (User.getMyPin() ?? ""), timeCall: String(Date().currentTimeMillis()), attachment_type: MessageScope.MISSED_CALL)
+                            }
+                        } else if UIApplication.shared.visibleViewController is QmeraAudioViewController || UIApplication.shared.visibleViewController is QmeraVideoViewController {
+                            var dataMessage: [AnyHashable : Any] = [:]
+                            dataMessage["call_cancel"] = true
+                            NotificationCenter.default.post(name: NSNotification.Name(rawValue: Nexilis.callFCM), object: nil, userInfo: dataMessage)
+                        }
+                    }
                 }
             }
         }
@@ -1035,6 +1076,66 @@ public class APIS: NSObject {
         }
     }
     
+    private static func getMessageById(id: String) {
+        DispatchQueue.global().async {
+//            Nexilis.sendStateToServer(s: "send ack from apn")
+            DispatchQueue.global().async {
+                let parameter: [String : Any] = [
+                    "pin": User.getMyPin() ?? "",
+                    "message_id": id
+                ]
+                Utils.postDataWithCookiesAndUserAgent(from: URL(string: Utils.getDomainOpr() + "pull_notification")!, parameter: parameter, isFormData: true) { data, response, error in
+                    if let data = data {
+                        do {
+                            if let dataString = String(data: data, encoding: .utf8) {
+                                if let jsonObj = try! JSONSerialization.jsonObject(with: dataString.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [String: Any] {
+                                    let dataObj = jsonObj["data"] as? String ?? ""
+                                    let message = TMessage(data: dataObj)
+                                    Nexilis.saveMessage(message: message, withStatus: false, fromAPNS: true)
+                                    ackAPN(id: id)
+                                }
+                            }
+                        } catch {
+                            
+                        }
+                    }
+                }
+            }
+//            do {
+//                if API.nGetCLXConnState() == 0 {
+//                    let id = Utils.getConnectionID()
+//                    try API.initConnection(sAPIK: Nexilis.sAPIKey, cbiI: Callback(), sTCPAddr: Nexilis.ADDRESS, nTCPPort: Nexilis.PORT, sUserID: id, sStartWH: "09:00")
+//                    while API.nGetCLXConnState() == 0 {
+//                        print("nGetCLXConnState: 0")
+//                        Thread.sleep(forTimeInterval: 1)
+//                    }
+//                    print("nGetCLXConnState: lewat")
+//                    getMessage()
+//                } else {
+//                    getMessage()
+//                }
+//                func getMessage() {
+//                    if let result = Nexilis.writeSync(message: CoreMessage_TMessageBank.getMessageById(messageId: id), timeout: 30 * 1000) {
+//                        print("result: \(result.toLogString())")
+//                        if result.isOk() {
+//                            let respData = result.getBody(key: CoreMessage_TMessageKey.DATA)
+//                            if let data = Data(base64Encoded: respData, options: .ignoreUnknownCharacters),
+//                               let decodedString = String(data: data, encoding: .utf8) {
+//                                let message = TMessage(data: decodedString)
+//                                print("message: \(message.toLogString())")
+//                            }
+//                        } else {
+//                            
+//                        }
+//                    } else {
+//                    }
+//                }
+//            } catch {
+//                
+//            }
+        }
+    }
+    
     public static func addNotificationNexilis(_ message: TMessage) {
         var text = message.getBody(key: CoreMessage_TMessageKey.MESSAGE_TEXT)
         text = text.toNormalString()
@@ -1145,8 +1246,7 @@ public class APIS: NSObject {
         }
         content.userInfo = ["id" : threadIdentifier, "type" : type]
         content.sound = UNNotificationSound(named: UNNotificationSoundName("\(nameSound).mp3"))
-        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
-        let request = UNNotificationRequest(identifier: messageId, content: content, trigger: trigger)
+        let request = UNNotificationRequest(identifier: messageId, content: content, trigger: nil)
         center.add(request) { error in
             if let error = error {
                 print("Error scheduling notification: \(error.localizedDescription)")
@@ -1233,37 +1333,26 @@ public class APIS: NSObject {
             } else {
                 let userInfo = response.notification.request.content.userInfo
                 DispatchQueue.main.async {
-                    if let payload = userInfo["payload"] as? [String: Any] {
-                        if let messagePayload = payload["message"] as? [String: Any] {
-                            if let data = messagePayload["data"] as? [String: Any] {
-                                let code = data["nx_code"] as? String ?? ""
-                                if code == "CL01" {
-                                    if let message = data["bodies"] as? [String: String] {
-                                        let idAck = data["message_id"] as? String ?? ""
-                                        let messageToSave = TMessage()
-                                        messageToSave.mBodies = message
-                                        do {
-                                            var messageExist = false
-                                            Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                                                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select message_id from MESSAGE where message_id = '\(message[CoreMessage_TMessageKey.MESSAGE_ID] ?? "")'"), cursor.next() {
-                                                    messageExist = true
-                                                    cursor.close()
-                                                }
-                                            })
-                                            if messageExist {
-                                                ackAPN(id: idAck)
-                                                return
-                                            }
-                                        } catch {
-                                            print("error saving message: \(error)")
-                                        }
-                                        ackAPN(id: idAck)
-                                        Nexilis.saveMessage(message: messageToSave, withStatus: false, fromAPNS: true)
-                                        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
-                                    }
-                                }
+                    if let message_id = userInfo[CoreMessage_TMessageKey.MESSAGE_ID] as? String {
+                        var f_pin = ""
+                        var l_pin = ""
+                        var message_scope_id = ""
+                        var pin = ""
+                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, l_pin, message_scope_id from MESSAGE where message_id = '\(message_id)'"), cursor.next() {
+                                f_pin = cursor.string(forColumnIndex: 0) ?? ""
+                                l_pin = cursor.string(forColumnIndex: 1) ?? ""
+                                message_scope_id = cursor.string(forColumnIndex: 2) ?? ""
+                                pin = f_pin == User.getMyPin() ? l_pin : f_pin
+                                cursor.close()
+                            }
+                        })
+                        if let navigationC = UIApplication.shared.visibleViewController as? UINavigationController {
+                            if navigationC.viewControllers[navigationC.viewControllers.count - 1] is EditorPersonal || navigationC.viewControllers[navigationC.viewControllers.count - 1] is EditorGroup {
+                                navigationC.popViewController(animated: false)
                             }
                         }
+                        showEditorOrCallFromAPN(pin, message_scope_id == "4" ? "1" : "0", "CL01")
                     }
                 }
             }
@@ -1422,6 +1511,7 @@ public class APIS: NSObject {
     
     public static var notifTimer = Timer()
     public static var stopNotif = false
+    public static var afterEnterForeground = false
     public static func enterForeground() {
         APIS.checkNotificationPermission(completion: { isAllowed in
             if !isAllowed {
@@ -1448,6 +1538,15 @@ public class APIS: NSObject {
         checkDataForShareExtension()
         UIApplication.shared.applicationIconBadgeNumber = 0
         UNUserNotificationCenter.current().removeAllDeliveredNotifications()
+        afterEnterForeground = true
+    }
+    
+    public static func willEnterForeground() {
+        if APIS.uuidCall != nil {
+            CallManager.shared.endCall(uuid: APIS.uuidCall!) {
+                APIS.uuidCall = nil
+            }
+        }
     }
     
     private static func checkNotificationPermission(completion: @escaping (Bool) -> Void) {

+ 1 - 8
NexilisLite/NexilisLite/Source/Callback.swift

@@ -38,20 +38,13 @@ class Callback : CallBack {
         //print(nState,"/",sMessage)
         if nState == Nexilis.AUDIO_CALL_INCOMING || nState == Nexilis.VIDEO_CALL_INCOMING {
             if let delegate = Nexilis.shared.callDelegate {
-                if !Nexilis.showLibraryNotification {
+                if !Nexilis.showLibraryNotification || Nexilis.callAPNActivated {
                     delegate.onStatusCall(state: nState, message: sMessage)
                 } else {
                     delegate.onIncomingCall(state: nState, message: sMessage)
                 }
             }
         } else {
-//            if nstate == Nexilis.AUDIO_CALL_END {
-//                let pin = sMessage.split(separator: ",")[0]
-//                if let call = Palio.shared.callManager.call(with: String(pin)), !call.hasVideo, !call.hasEnded {
-//                    call.reconnectingCall()
-//                    return 1
-//                }
-//            }
             if let delegate = Nexilis.shared.callDelegate {
                 delegate.onStatusCall(state: nState, message: sMessage)
             }

+ 0 - 9
NexilisLite/NexilisLite/Source/CoreMessage_TMessageBank.swift

@@ -10,15 +10,6 @@ import Foundation
 
 public class CoreMessage_TMessageBank {
     
-    public static func endCall(pin: String) -> TMessage {
-        let tmessage = TMessage()
-        tmessage.mCode = CoreMessage_TMessageCode.END_CALL
-        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
-        tmessage.mPIN = User.getMyPin()!
-        tmessage.mBodies[CoreMessage_TMessageKey.F_PIN] = pin
-        return tmessage
-    }
-    
     public static func checkCallStatus(pin: String) -> TMessage {
         let tmessage = TMessage()
         tmessage.mCode = CoreMessage_TMessageCode.ASKING_FOR_END_CALL

+ 15 - 17
NexilisLite/NexilisLite/Source/Extension.swift

@@ -428,15 +428,6 @@ extension NSObject {
                     }
                     
                     DispatchQueue.main.async {
-                        if tableView != nil {
-                            if let indexPath = indexPath,
-                               indexPath.section < tableView!.numberOfSections,
-                               indexPath.row < tableView!.numberOfRows(inSection: indexPath.section) {
-                                tableView!.beginUpdates()
-                                tableView!.reloadRows(at: [indexPath], with: .none)
-                                tableView!.endUpdates()
-                            }
-                        }
                         if type(of: self).urlStore[tmpAddress] == name && tableView == nil {
                             if FileManager().fileExists(atPath: file.path) {
                                 let image = UIImage(contentsOfFile: file.path)?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
@@ -456,6 +447,14 @@ extension NSObject {
                                 }
                             }
                         }
+                        guard let tableView = tableView else { return }
+                        tableView.beginUpdates()
+                        if let indexPath = indexPath,
+                           indexPath.section < tableView.numberOfSections,
+                           indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {
+                            tableView.reloadRows(at: [indexPath], with: .none)
+                        }
+                        tableView.endUpdates()
                     }
                 }
             }
@@ -467,15 +466,14 @@ extension NSObject {
                 }
                 
                 DispatchQueue.main.async {
-                    if tableView != nil {
-                        if let indexPath = indexPath,
-                           indexPath.section < tableView!.numberOfSections,
-                           indexPath.row < tableView!.numberOfRows(inSection: indexPath.section) {
-                            tableView!.beginUpdates()
-                            tableView!.reloadRows(at: [indexPath], with: .none)
-                            tableView!.endUpdates()
-                        }
+                    guard let tableView = tableView else { return }
+                    tableView.beginUpdates()
+                    if let indexPath = indexPath,
+                       indexPath.section < tableView.numberOfSections,
+                       indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {
+                        tableView.reloadRows(at: [indexPath], with: .none)
                     }
+                    tableView.endUpdates()
                 }
             }
         }

+ 68 - 36
NexilisLite/NexilisLite/Source/IncomingThread.swift

@@ -309,8 +309,15 @@ class IncomingThread {
     }
     
     private func sendOnlineUser(message: TMessage) -> Void {
-        if let packetId = message.mBodies[CoreMessage_TMessageKey.PACKET_ID] {
-            _ = Nexilis.responseString(packetId: packetId, message: "01", timeout: 3000)
+        DispatchQueue.main.async {
+            if !APIS.checkAppStateisBackground() {
+                let fPIn = message.getPIN()
+                if let packetId = message.mBodies[CoreMessage_TMessageKey.PACKET_ID] {
+                    if fPIn != User.getMyPin() {
+                        _ = Nexilis.responseString(packetId: packetId, message: "01", timeout: 3000)
+                    }
+                }
+            }
         }
     }
     
@@ -568,6 +575,19 @@ class IncomingThread {
                                     if FileManager().fileExists(atPath: file.path) {
                                         profileImage.image = UIImage(contentsOfFile: file.path)
                                         profileImage.backgroundColor = .clear
+                                    } else if FileEncryption.shared.isSecureExists(filename: profile) {
+                                        do {
+                                            if var data = try FileEncryption.shared.readSecure(filename: profile) {
+                                                let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                                if dataDecrypt != nil {
+                                                    data = dataDecrypt!
+                                                }
+                                                profileImage.image = UIImage(data: data)
+                                                profileImage.backgroundColor = .clear
+                                            }
+                                        } catch {
+                                            
+                                        }
                                     } else {
                                         Download().startHTTP(forKey: profile) { (name, progress) in
                                             guard progress == 100 else {
@@ -575,8 +595,20 @@ class IncomingThread {
                                             }
                                             
                                             DispatchQueue.main.async {
-                                                profileImage.image = UIImage(contentsOfFile: file.path)
-                                                profileImage.backgroundColor = .clear
+                                                if FileEncryption.shared.isSecureExists(filename: profile) {
+                                                    do {
+                                                        if var data = try FileEncryption.shared.readSecure(filename: profile) {
+                                                            let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                                            if dataDecrypt != nil {
+                                                                data = dataDecrypt!
+                                                            }
+                                                            profileImage.image = UIImage(data: data)
+                                                            profileImage.backgroundColor = .clear
+                                                        }
+                                                    } catch {
+                                                        
+                                                    }
+                                                }
                                                 Nexilis.shared.floating.show(queuePosition: .front, bannerPosition: .top, queue: NotificationBannerQueue(maxBannersOnScreenSimultaneously: 1), on: nil, edgeInsets: UIEdgeInsets(top: 8.0, left: 8.0, bottom: 0, right: 8.0), cornerRadius: 8.0, shadowColor: .clear, shadowOpacity: .zero, shadowBlurRadius: .zero, shadowCornerRadius: .zero, shadowOffset: .zero, shadowEdgeInsets: nil)
                                                 return
                                             }
@@ -671,12 +703,12 @@ class IncomingThread {
     }
     
     private func endCall(message: TMessage) {
-        if let call = Nexilis.shared.callManager.call(with: message.mPIN) {
-            call.isReceiveEnd = true
-            DispatchQueue.main.async {
-                Nexilis.shared.callManager.end(call: call)
-            }
-        }
+//        if let call = Nexilis.shared.callManager.call(with: message.mPIN) {
+//            call.isReceiveEnd = true
+//            DispatchQueue.main.async {
+//                Nexilis.shared.callManager.end(call: call)
+//            }
+//        }
         ack(message: message)
     }
     
@@ -1277,24 +1309,24 @@ class IncomingThread {
             ack(message: message)
             return
         }
-        var messageExist = false
-        Database.shared.database?.inTransaction({ (fmdb, rollback) in
-            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select message_id from MESSAGE where message_id = '\(message.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID))'"), cursor.next() {
-                messageExist = true
-                cursor.close()
-            }
-        })
-        DispatchQueue.main.async {
-            if APIS.checkAppStateisBackground() {
-                if !messageExist {
-                    let message_id = message.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID)
-                    DispatchQueue.global().async {
-                        _ = Nexilis.write(message: CoreMessage_TMessageBank.getAckMessage(messageId: message_id))
-                    }
-                    APIS.addNotificationNexilis(message)
-                }
-            }
-        }
+//        var messageExist = false
+//        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+//            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select message_id from MESSAGE where message_id = '\(message.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID))'"), cursor.next() {
+//                messageExist = true
+//                cursor.close()
+//            }
+//        })
+//        DispatchQueue.main.async {
+//            if APIS.checkAppStateisBackground() {
+//                if !messageExist {
+//                    let message_id = message.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID)
+//                    DispatchQueue.global().async {
+//                        _ = Nexilis.write(message: CoreMessage_TMessageBank.getAckMessage(messageId: message_id))
+//                    }
+//                    APIS.addNotificationNexilis(message)
+//                }
+//            }
+//        }
         let media = message.getMedia()
         let thumb_id = message.getBody(key: CoreMessage_TMessageKey.THUMB_ID)
         if media.count != 0 {
@@ -1315,19 +1347,19 @@ class IncomingThread {
         } else {
             Nexilis.saveMessage(message: message, withStatus: false)
         }
-        DispatchQueue.main.async { [self] in
-            if APIS.checkAppStateisBackground() {
-                APIS.addNotificationNexilis(message)
-                ackAPN(id: message.mStatus)
-            }
-        }
+//        DispatchQueue.main.async { [self] in
+//            if APIS.checkAppStateisBackground() {
+//                APIS.addNotificationNexilis(message)
+//                ackAPN(id: message.mStatus)
+//            }
+//        }
         //print("save message incoming")
         ack(message: message)
     }
     
     private func ackAPN(id: String) {
         DispatchQueue.global().async {
-            Nexilis.sendStateToServer(s: "send ack from apn")
+//            Nexilis.sendStateToServer(s: "send ack from apn")
             DispatchQueue.global().async {
                 let parameter: [String : Any] = [
                     "pin": User.getMyPin() ?? "",
@@ -1664,7 +1696,7 @@ class IncomingThread {
                                 do {
                                     let documentDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
                                     let file = documentDir.appendingPathComponent(imageId)
-                                    if !FileManager().fileExists(atPath: file.path) {
+                                    if !FileManager().fileExists(atPath: file.path) || !FileEncryption.shared.isSecureExists(filename: imageId) {
                                         Download().startHTTP(forKey: imageId) { (name, progress) in}
                                     }
                                 } catch {}

+ 1 - 0
NexilisLite/NexilisLite/Source/Model/User.swift

@@ -25,6 +25,7 @@ public class User: Model {
     
     public var isSelected: Bool = false
     public var isMuted: Bool = false
+    public var isConnected: Bool = false
     
     public init(pin: String) {
         self.pin = pin

+ 29 - 40
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -43,10 +43,6 @@ public class Nexilis: NSObject {
     public static var showFB = false
     public static var fromMAB = false
     
-    let callManager = CallManager()
-    
-    var providerDelegate: CallProviderDelegate?
-    
     public static let shared = Nexilis()
     
 //    public static var broadcastTimer = Timer()
@@ -138,7 +134,6 @@ public class Nexilis: NSObject {
         messageDelegate = self
         groupDelegate = self
         personInfoDelegate = self
-        providerDelegate = CallProviderDelegate(callManager: callManager)
     }
     
     public static func connect(apiKey: String, delegate: ConnectDelegate, showButton: Bool = true, fromMAB: Bool = false) {
@@ -234,7 +229,7 @@ public class Nexilis: NSObject {
                                     do {
                                         let documentDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
                                         let file = documentDir.appendingPathComponent(cursorData.string(forColumnIndex: 0)!)
-                                        if !FileManager().fileExists(atPath: file.path) {
+                                        if !FileManager().fileExists(atPath: file.path) || !FileEncryption.shared.isSecureExists(filename: cursorData.string(forColumnIndex: 0)!) {
                                             Download().startHTTP(forKey: cursorData.string(forColumnIndex: 0)!) { (name, progress) in}
                                         }
                                     } catch {}
@@ -1423,6 +1418,7 @@ public class Nexilis: NSObject {
                         "is_forwarded_message" : is_forwarded_message
                     ], replace: true)
                 } catch {
+                    print("ERROR: \(error)")
                     rollback.pointee = true
                     //print(error)
                 }
@@ -2351,10 +2347,6 @@ public var uuidOngoing = UUID()
 
 extension Nexilis: CallDelegate {
     
-    func displayIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((Error?) -> Void)? = nil) {
-        providerDelegate?.reportIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo, completion: completion)
-    }
-    
     public func onIncomingCall(state: Int, message: String) {
         DispatchQueue.main.async {
             let idMe = User.getMyPin()!
@@ -2401,11 +2393,6 @@ extension Nexilis: CallDelegate {
                     })
                     return
                 }
-//                    let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
-//                    uuidOngoing = UUID()
-//                    self.displayIncomingCall(uuid: uuidOngoing, handle: String(deviceId), hasVideo: false) { error in
-//                        UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
-//                    }
                 let controller = QmeraAudioViewController()
                 controller.user = User.getData(pin: String(deviceId))
                 controller.isOutgoing = false
@@ -2426,7 +2413,6 @@ extension Nexilis: CallDelegate {
                 } else {
                     UIApplication.shared.visibleViewController?.present(controller, animated: true, completion: nil)
                 }
-//                    API.receiveCCall(sParty: String(deviceId))
             } else if (state == Nexilis.VIDEO_CALL_INCOMING && message.split(separator: ",")[1] != "joining Ac.room on channel 0" && message.split(separator: ",")[1] != "joining Vc.room on channel 0") {
                 if Nexilis.callAPNActivated || APIS.checkAppStateisBackground() {
                     return
@@ -2464,28 +2450,6 @@ extension Nexilis: CallDelegate {
     }
     
     public func onStatusCall(state: Int, message: String) {
-//        let r = message.split(separator: ",")
-//        if state == Nexilis.AUDIO_CALL_RINGING {
-//            if let call = callManager.call(with: String(r[0])) {
-//                //print("onStatusCall:connectingCall")
-//                DispatchQueue.main.async {
-//                    call.connectingCall()
-//                }
-//            }
-//        } else if state == Nexilis.AUDIO_CALL_OFFHOOK {
-////            if let call = callManager.call(with: String(r[1])) {
-//                //print("onStatusCall:answerCall")
-////                DispatchQueue.main.async {
-////                    call.answerCall()
-////                }
-////            }
-//        } else if state == Nexilis.AUDIO_CALL_END {
-//            DispatchQueue.main.async {
-//                if QmeraAudioViewController().viewIfLoaded?.window == nil {
-//                    Nexilis.shared.callManager.end(call: Call(uuid: uuidOngoing))
-//                }
-//            }
-//        }
         var dataCall: [AnyHashable : Any] = [:]
         dataCall["state"] = state
         dataCall["message"] = message
@@ -3951,6 +3915,19 @@ extension Nexilis: MessageDelegate {
                                 if FileManager().fileExists(atPath: file.path) {
                                     profileImage.image = UIImage(contentsOfFile: file.path)
                                     profileImage.backgroundColor = .clear
+                                } else if FileEncryption.shared.isSecureExists(filename: profile) {
+                                    do {
+                                        if var data = try FileEncryption.shared.readSecure(filename: profile) {
+                                            let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                            if dataDecrypt != nil {
+                                                data = dataDecrypt!
+                                            }
+                                            profileImage.image = UIImage(data: data)
+                                            profileImage.backgroundColor = .clear
+                                        }
+                                    } catch {
+                                        
+                                    }
                                 } else {
                                     Download().startHTTP(forKey: profile) { (name, progress) in
                                         guard progress == 100 else {
@@ -3958,8 +3935,20 @@ extension Nexilis: MessageDelegate {
                                         }
                                         
                                         DispatchQueue.main.async { [self] in
-                                            profileImage.image = UIImage(contentsOfFile: file.path)
-                                            profileImage.backgroundColor = .clear
+                                            if FileEncryption.shared.isSecureExists(filename: profile) {
+                                                do {
+                                                    if var data = try FileEncryption.shared.readSecure(filename: profile) {
+                                                        let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                                        if dataDecrypt != nil {
+                                                            data = dataDecrypt!
+                                                        }
+                                                        profileImage.image = UIImage(data: data)
+                                                        profileImage.backgroundColor = .clear
+                                                    }
+                                                } catch {
+                                                    
+                                                }
+                                            }
                                             if !onGoingCC.isEmpty {
                                                 floating.autoDismiss = false
                                             }

+ 1 - 1
NexilisLite/NexilisLite/Source/Utils.swift

@@ -496,7 +496,7 @@ public final class Utils {
                 return showNSMutableAttributedString(("📹 " + "Video".localized()))
             }
         }
-        else if !chat.file.isEmpty {
+        else if !chat.file.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
             if chat.messageScope == "18" {
                 return showNSMutableAttributedString(("📄 Form"))
             }

+ 9 - 9
NexilisLite/NexilisLite/Source/View/Call/AudioViewController.swift

@@ -80,7 +80,7 @@ class AudioViewController: UIViewController {
             })
             
             if isOutgoing {
-                Nexilis.shared.callManager.startCall(handle: pin)
+//                Nexilis.shared.callManager.startCall(handle: pin)
             }
         }
     }
@@ -108,14 +108,14 @@ class AudioViewController: UIViewController {
     
     @objc func didEnd(sender: AnyObject?) {
         //print("didEnd:\(pin)")
-        if let pin = self.pin, let call = Nexilis.shared.callManager.call(with: pin) {
-            Nexilis.shared.callManager.end(call: call)
-            if isOutgoing {
-                navigationController?.popViewController(animated: true)
-            } else {
-                navigationController?.dismiss(animated: true, completion: nil)
-            }
-        }
+//        if let pin = self.pin, let call = Nexilis.shared.callManager.call(with: pin) {
+//            Nexilis.shared.callManager.end(call: call)
+//            if isOutgoing {
+//                navigationController?.popViewController(animated: true)
+//            } else {
+//                navigationController?.dismiss(animated: true, completion: nil)
+//            }
+//        }
     }
     
     @objc func onStatusCall(_ notification: NSNotification) {

+ 0 - 105
NexilisLite/NexilisLite/Source/View/Call/Call.swift

@@ -1,105 +0,0 @@
-//
-//  Call.swift
-//  FloatingButtonApp
-//
-//  Created by Yayan Dwi on 10/08/21.
-//
-
-import Foundation
-
-final class Call: ObservableObject {
-    
-    let uuid: UUID
-    let isOutgoing: Bool
-    var handle: String?
-    var hasVideo: Bool = false
-    var isReceiveEnd: Bool = false
-    
-    init(uuid: UUID, isOutgoing: Bool = false) {
-        self.uuid = uuid
-        self.isOutgoing = isOutgoing
-    }
-    
-    var stateDidChange: (() -> Void)?
-    var hasStartedConnectingDidChange: (() -> Void)?
-    var hasConnectedDidChange: (() -> Void)?
-    var hasEndedDidChange: (() -> Void)?
-    
-    var connectingDate: Date? {
-        didSet {
-            stateDidChange?()
-            hasStartedConnectingDidChange?()
-        }
-    }
-    
-    var connectDate: Date? {
-        didSet {
-            stateDidChange?()
-            hasConnectedDidChange?()
-        }
-    }
-    
-    var endDate: Date? {
-        didSet {
-            stateDidChange?()
-            hasEndedDidChange?()
-        }
-    }
-    
-    var isOnHold = false {
-        didSet {
-            stateDidChange?()
-        }
-    }
-    
-    var hasStartedConnecting: Bool {
-        get {
-            return connectingDate != nil
-        }
-        set {
-            connectingDate = newValue ? Date() : nil
-        }
-    }
-    
-    var hasConnected: Bool {
-        get {
-            return connectDate != nil
-        }
-        set {
-            connectDate = newValue ? Date() : nil
-        }
-    }
-    
-    var hasEnded: Bool {
-        get {
-            return endDate != nil
-        }
-        set {
-            endDate = newValue ? Date() : nil
-        }
-    }
-    
-    var duration: TimeInterval {
-        guard let connectDate = connectDate else {
-            return 0
-        }
-        
-        return Date().timeIntervalSince(connectDate)
-    }
-    
-    func connectingCall() {
-        hasStartedConnecting = true
-    }
-    
-    func answerCall() {
-        hasConnected = true
-    }
-    
-    func endCall() {
-        hasEnded = true
-    }
-    
-    func reconnectingCall() {
-        hasEnded = false
-    }
-}

+ 11 - 0
NexilisLite/NexilisLite/Source/View/Call/CallLogVC.swift

@@ -38,10 +38,15 @@ public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDele
         tabBarController?.navigationController?.setNavigationBarHidden(true, animated: false)
         navigationController?.navigationBar.prefersLargeTitles = true
         navigationController?.navigationItem.largeTitleDisplayMode = .always
+        navigationController?.navigationBar.backgroundColor = .clear
+        navigationController?.navigationBar.tintColor = .black
+        navigationController?.navigationBar.overrideUserInterfaceStyle = .light
+        self.setNeedsStatusBarAppearanceUpdate()
         let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.foregroundColor: self.traitCollection.userInterfaceStyle == .dark ? .white : UIColor.black, NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 16)]
         let largeAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.foregroundColor: self.traitCollection.userInterfaceStyle == .dark ? .white : UIColor.black, NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 34)]
         let appearance = UINavigationBarAppearance()
         appearance.configureWithTransparentBackground()
+        appearance.backgroundColor = .clear
         appearance.titleTextAttributes = attributes
         appearance.largeTitleTextAttributes = largeAttributes
         navigationController?.navigationBar.standardAppearance = appearance
@@ -86,6 +91,12 @@ public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDele
         }
     }
     
+    public override func viewDidAppear(_ animated: Bool) {
+        if Nexilis.floatingButton.isHidden {
+            Nexilis.floatingButton.isHidden = false
+        }
+    }
+    
     private func refresh() {
         getData()
         

+ 151 - 62
NexilisLite/NexilisLite/Source/View/Call/CallManager.swift

@@ -7,86 +7,175 @@
 
 import Foundation
 import CallKit
+import nuSDKService
 
-final class CallManager: NSObject, ObservableObject {
+public class CallManager: NSObject, ObservableObject {
     
-    let callController = CXCallController()
-    
-    func startCall(handle: String, video: Bool = false) {
-        let cx = CXHandle(type: .phoneNumber, value: handle)
-        let startCallAction = CXStartCallAction(call: UUID(), handle: cx)
-        
-        startCallAction.isVideo = video
-        
-        let transaction = CXTransaction()
-        transaction.addAction(startCallAction)
+    public static let shared = CallManager()
+    private var activeCalls: [UUID: CallInfo] = [:]
         
-        requestTransaction(transaction)
-    }
+    private let provider: CXProvider
+    private let callController = CXCallController()
     
-    func end(call: Call) {
-        let endCallAction = CXEndCallAction(call: call.uuid)
-        let transaction = CXTransaction()
-        transaction.addAction(endCallAction)
+    override init() {
+        let providerConfiguration = CXProviderConfiguration(localizedName: Bundle.main.infoDictionary?["CFBundleName"] as! String)
+        providerConfiguration.supportsVideo = true
+        providerConfiguration.maximumCallsPerCallGroup = 1
+        providerConfiguration.supportedHandleTypes = [.generic]
+        providerConfiguration.iconTemplateImageData = nil
         
-        requestTransaction(transaction)
-    }
-    
-    func setOnHoldStatus(for call: Call, to onHold: Bool) {
-        let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold)
-        let transaction = CXTransaction()
-        transaction.addAction(setHeldCallAction)
         
-        requestTransaction(transaction)
+        provider = CXProvider(configuration: providerConfiguration)
+        super.init()
+        self.provider.setDelegate(self, queue: nil)
     }
     
-    func startGroupCall(uuid1: UUID) {
-        let call1UUID = calls[0].uuid
-        let call2UUID = uuid1
-        let mergeCallAction = CXSetGroupCallAction(call: call1UUID, callUUIDToGroupWith: call2UUID)
+    public func reportIncomingCall(uuid: UUID, callerName: String, callerId: String, isVideo: Bool) {
+        let update = CXCallUpdate()
+        update.remoteHandle = CXHandle(type: .generic, value: callerId)
+        update.localizedCallerName = callerName
+        update.hasVideo = isVideo
+        activeCalls[uuid] = CallInfo(uuid: uuid, callerId: callerId, callerName: callerName, isVideo: isVideo, isAccepted: false)
 
-        let transaction = CXTransaction()
-        transaction.addAction(mergeCallAction)
-        
-        requestTransaction(transaction)
+        provider.reportNewIncomingCall(with: uuid, update: update) { error in
+            if let error = error {
+                print("Error reporting incoming call: \(error.localizedDescription)")
+            }
+        }
     }
     
-    private func requestTransaction(_ transaction: CXTransaction) {
+    public func endCall(uuid: UUID, completion: @escaping () -> ()) {
+        let endCallAction = CXEndCallAction(call: uuid)
+        let transaction = CXTransaction(action: endCallAction)
+
         callController.request(transaction) { error in
-//            if let error = error {
-//                //print("Error requesting transaction:", error.localizedDescription)
-//            } else {
-//                //print("Requested transaction successfully")
-//            }
+            if let error = error {
+                print("Failed to end call: \(error.localizedDescription)")
+            } else {
+                completion()
+            }
         }
     }
     
-    @Published private(set) var calls = [Call]()
-    
-    func callWithUUID(uuid: UUID) -> Call? {
-        guard let index = calls.firstIndex(where: { $0.uuid == uuid }) else { return nil }
-        
-        return calls[index]
-    }
-    
-    func call(with handle: String) -> Call? {
-        guard let index = calls.firstIndex(where: { $0.handle == handle }) else { return nil }
-        
-        return calls[index]
-    }
-    
-    func addCall(_ call: Call) {
-        calls.append(call)
+}
+
+extension CallManager: CXProviderDelegate {
+    public func providerDidReset(_ provider: CXProvider) {
     }
     
-    func removeCall(_ call: Call) {
-        guard let index = calls.firstIndex(where: { $0 === call }) else { return }
-        
-        calls.remove(at: index)
+    public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
+        let uuid = action.callUUID
+        if let callInfo = activeCalls[uuid] {
+            self.activeCalls[uuid]?.isAccepted = true
+            if callInfo.isVideo {
+                let videoController = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "videoVCQmera") as! QmeraVideoViewController
+                videoController.fPin = callInfo.callerId
+                videoController.isInisiator = false
+                videoController.autoAcceptAPN = true
+                let navigationController = CustomNavigationController(rootViewController: videoController)
+                navigationController.modalPresentationStyle = .fullScreen
+                if UIApplication.shared.visibleViewController is UIAlertController {
+                    let vc = UIApplication.shared.visibleViewController as! UIAlertController
+                    vc.dismiss(animated: true, completion: {
+                        if UIApplication.shared.visibleViewController?.navigationController != nil {
+                            UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+                        } else {
+                            UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+                        }
+                    })
+                    return
+                }
+                if UIApplication.shared.visibleViewController?.navigationController != nil {
+                    UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+                } else {
+                    UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+                }
+            } else {
+                let controller = QmeraAudioViewController()
+                controller.isOutgoing = false
+                controller.user = User.getData(pin: callInfo.callerId)
+                controller.autoAcceptAPN = true
+                controller.modalPresentationStyle = .overCurrentContext
+                if UIApplication.shared.visibleViewController is UIAlertController {
+                    let vc = UIApplication.shared.visibleViewController as! UIAlertController
+                    vc.dismiss(animated: true, completion: {
+                        if UIApplication.shared.visibleViewController?.navigationController != nil {
+                            UIApplication.shared.visibleViewController?.navigationController?.present(controller, animated: true, completion: nil)
+                        } else {
+                            UIApplication.shared.visibleViewController?.present(controller, animated: true, completion: nil)
+                        }
+                    })
+                    return
+                }
+                if UIApplication.shared.visibleViewController?.navigationController != nil {
+                    UIApplication.shared.visibleViewController?.navigationController?.present(controller, animated: true, completion: nil)
+                } else {
+                    UIApplication.shared.visibleViewController?.present(controller, animated: true, completion: nil)
+                }
+            }
+        }
+        action.fulfill()
     }
     
-    func removeAllCalls() {
-        calls.removeAll()
+    public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
+        if Nexilis.callAPNActivated {
+            let uuid = action.callUUID
+            if let callInfo = activeCalls[uuid] {
+                if !callInfo.isAccepted {
+                    DispatchQueue.global().async {
+                        do {
+                            if API.nGetCLXConnState() == 0 {
+                                let id = Utils.getConnectionID()
+                                try API.initConnection(sAPIK: Nexilis.sAPIKey, cbiI: Callback(), sTCPAddr: Nexilis.ADDRESS, nTCPPort: Nexilis.PORT, sUserID: id, sStartWH: "09:00")
+                                while API.nGetCLXConnState() == 0 {
+                                    Thread.sleep(forTimeInterval: 1)
+                                }
+                                sendCancel()
+                            } else {
+                                sendCancel()
+                            }
+                            func sendCancel() {
+                                if let result = Nexilis.writeSync(message: CoreMessage_TMessageBank.getCancelCall(fPin: callInfo.callerId, type: !callInfo.isVideo ? "1" : "2"), timeout: 30 * 1000) {
+                                    if result.isOk() {
+                                        let center = UNUserNotificationCenter.current()
+                                        var textCall = ""
+                                        if !callInfo.isVideo {
+                                            textCall = "audio"
+                                        } else {
+                                            textCall = "video"
+                                        }
+                                        let content = UNMutableNotificationContent()
+                                        content.title = callInfo.callerName
+                                        content.body = "☎️ Missed \(textCall) call".localized()
+                                        content.userInfo = ["id" : callInfo.callerId, "type" : "CL02", "callType": callInfo.isVideo ? "2" : "1"]
+                                        content.sound = nil
+                                        let request = UNNotificationRequest(identifier: callInfo.callerId, content: content, trigger: nil)
+                                        center.add(request) { error in
+                                            if let error = error {
+                                                print("Error scheduling notification: \(error.localizedDescription)")
+                                            }
+                                        }
+                                        Nexilis.saveMessageCall(idCall: (User.getMyPin() ?? "") + CoreMessage_TMessageUtil.getTID(), textMessage: "Missed \(textCall) call".localized() + " at 0", fPin: callInfo.callerId, lPin: (User.getMyPin() ?? ""), timeCall: String(Date().currentTimeMillis()), attachment_type: MessageScope.MISSED_CALL)
+                                    }
+                                }
+                            }
+                        } catch {
+                            
+                        }
+                    }
+                    APIS.uuidCall = nil
+                    Nexilis.callAPNActivated = false
+                }
+            }
+        }
+        action.fulfill()
     }
-    
+}
+
+struct CallInfo {
+    let uuid: UUID
+    let callerId: String
+    let callerName: String
+    let isVideo: Bool
+    var isAccepted: Bool
 }

+ 0 - 199
NexilisLite/NexilisLite/Source/View/Call/CallProviderDelegate.swift

@@ -1,199 +0,0 @@
-//
-//  CallProviderDelegate.swift
-//  FloatingButtonApp
-//
-//  Created by Yayan Dwi on 10/08/21.
-//
-
-import Foundation
-import UIKit
-import CallKit
-import AVFoundation
-import SwiftUI
-import nuSDKService
-
-class CallProviderDelegate: NSObject {
-    
-    static let providerConfiguration: CXProviderConfiguration = {
-        let providerConfiguration = CXProviderConfiguration()
-        providerConfiguration.maximumCallsPerCallGroup = 10
-        providerConfiguration.maximumCallGroups = 10
-        providerConfiguration.supportsVideo = true
-        providerConfiguration.supportedHandleTypes = [.phoneNumber]
-        //        providerConfiguration.ringtoneSound = "call_in.mp3"
-        return providerConfiguration
-    }()
-    
-    let callManager: CallManager
-    private let provider: CXProvider
-    
-    init(callManager: CallManager) {
-        self.callManager = callManager
-        provider = CXProvider(configuration: type(of: self).providerConfiguration)
-        super.init()
-        provider.setDelegate(self, queue: nil)
-    }
-    
-    func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((Error?) -> Void)? = nil) {
-        let update = CXCallUpdate()
-        if let user = User.getData(pin: handle) {
-            update.remoteHandle = CXHandle(type: .phoneNumber, value: user.fullName)
-        }
-        update.hasVideo = hasVideo
-        update.supportsGrouping = true
-        update.supportsUngrouping = true
-        update.supportsHolding = true
-        
-        provider.reportNewIncomingCall(with: uuid, update: update) { error in
-            if error == nil {
-                let call = Call(uuid: uuid)
-                call.handle = handle
-                call.hasVideo = hasVideo
-                
-                self.callManager.addCall(call)
-            }
-            completion?(error)
-        }
-    }
-    
-}
-
-extension CallProviderDelegate: CXProviderDelegate {
-    
-    func providerDidReset(_ provider: CXProvider) {
-        //print("providerDidReset...")
-        for call in callManager.calls {
-            call.endCall()
-        }
-        callManager.removeAllCalls()
-    }
-    
-    func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
-        //print("CXStartCallAction...\(action.callUUID)")
-        let call = Call(uuid: action.callUUID, isOutgoing: true)
-        call.handle = action.handle.value
-        call.hasStartedConnectingDidChange = { [weak self] in
-            self?.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: call.connectingDate)
-        }
-        call.hasConnectedDidChange = { [weak self] in
-            self?.provider.reportOutgoingCall(with: call.uuid, connectedAt: call.connectDate)
-        }
-        if self.callManager.calls.count > 0 {
-            self.callManager.setOnHoldStatus(for: self.callManager.calls.last!, to: true)
-        }
-        action.fulfill()
-        //print("JUMLAH START CALL \(self.callManager.calls.count)")
-        self.callManager.addCall(call)
-        if self.callManager.calls.count > 1 {
-            Nexilis.shared.callManager.startGroupCall(uuid1: call.uuid)
-            API.initiateCCall(sParty: call.handle)
-        }
-    }
-    
-    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
-        //print("CXAnswerCallAction...\(action.callUUID)")
-        guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
-            action.fail()
-            return
-        }
-        call.answerCall()
-        action.fulfill()
-        if call.hasVideo {
-            
-        } else {
-            let controller = QmeraAudioViewController()
-            controller.user = User.getData(pin: call.handle)
-            controller.isOnGoing = true
-            controller.isOutgoing = false
-            controller.modalPresentationStyle = .overCurrentContext
-            if UIApplication.shared.visibleViewController is UIAlertController {
-                let vc = UIApplication.shared.visibleViewController as! UIAlertController
-                vc.dismiss(animated: true, completion: {
-                    if UIApplication.shared.visibleViewController?.navigationController != nil {
-                        UIApplication.shared.visibleViewController?.navigationController?.present(controller, animated: true, completion: nil)
-                    } else {
-                        UIApplication.shared.visibleViewController?.present(controller, animated: true, completion: nil)
-                    }
-                })
-            } else {
-                if UIApplication.shared.visibleViewController?.navigationController != nil {
-                    UIApplication.shared.visibleViewController?.navigationController?.present(controller, animated: true, completion: nil)
-                } else {
-                    UIApplication.shared.visibleViewController?.present(controller, animated: true, completion: nil)
-                }
-            }
-            
-        }
-    }
-    
-    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
-        //print("CXEndCallAction...\(action.callUUID)")
-        guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
-            action.fail()
-            return
-        }
-        call.endCall()
-        action.fulfill()
-        self.callManager.removeCall(call)
-        //print("JUMLAH CALL \(self.callManager.calls.count)")
-        if self.callManager.calls.count == 0, !call.isReceiveEnd {
-            //print("MASUK TERMINATE CALL DELEGATE")
-            API.terminateCall(sParty: nil)
-            DispatchQueue.global().async {
-                if let pin = call.handle {
-                    _ = Nexilis.write(message: CoreMessage_TMessageBank.endCall(pin: pin))
-                }
-            }
-        }
-    }
-    
-    func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
-        //print("CXSetHeldCallAction...\(action.callUUID)")
-        guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
-            action.fail()
-            return
-        }
-        call.isOnHold = action.isOnHold
-//        if call.isOnHold {
-//            // pause??
-//        } else {
-//            // resume??
-//        }
-        action.fulfill()
-    }
-    
-    func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
-        //print("Timed out", #function)
-    }
-    
-    func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
-        //print("Received", #function)
-        
-        /*
-         Start call audio media, now that the audio session is activated,
-         after having its priority elevated.
-         */
-        if let call = self.callManager.calls.last {
-            if call.isOutgoing {
-                API.initiateCCall(sParty: call.handle)
-            } else {
-                API.receiveCCall(sParty: call.handle)
-            }
-        }
-    }
-    
-    func provider(_ provider: CXProvider, perform action: CXSetGroupCallAction) {
-        //print("CXGroupCallAction...\(action.callUUID)")
-        // perform merge call here where you merge ports of two call audio i/o
-        action.fulfill()
-    }
-    
-    func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
-        //print("Received", #function)
-        /*
-         Restart any non-call related audio now that the app's audio session is deactivated,
-         after having its priority restored to normal.
-         */
-    }
-    
-}

+ 1 - 1
NexilisLite/NexilisLite/Source/View/Call/QmeraAudioConference.swift

@@ -295,7 +295,7 @@ class QmeraAudioConference: UIViewController {
             } else {
                 self.users.append(user)
                 // Start Calling
-                Nexilis.shared.callManager.startCall(handle: user.pin)
+//                Nexilis.shared.callManager.startCall(handle: user.pin)
             }
         }
         controller.selectedUser.append(contentsOf: users)

+ 153 - 86
NexilisLite/NexilisLite/Source/View/Call/QmeraAudioViewController.swift

@@ -13,8 +13,8 @@ import MediaPlayer
 
 class QmeraAudioViewController: UIViewController {
     
-    static private let nMaxSPOn: Float! = 10.0
-    static private let nMaxSPOff: Float! = 9.0
+    static private var nMaxSPOn: Float! = 20.0
+    static private var nMaxSPOff: Float! = 20.0
     static private var volumeView: MPVolumeView!
     static private var lastVolume: Float! = AVAudioSession.sharedInstance().outputVolume
     static private var bSpeakerPhone: Bool! = false
@@ -48,8 +48,6 @@ class QmeraAudioViewController: UIViewController {
     var user: User?
     
     var isAddCall = ""
-    
-    private var isEndByMe = false
         
     private var users: [User] = [] {
         didSet {
@@ -267,13 +265,15 @@ class QmeraAudioViewController: UIViewController {
     static func turnSpeakerOn() {
         bSpeakerPhone = !bSpeakerPhone
         var bAudioEngineIsAvtive: Bool! = false
+        API.turnSpeakerPhone(bSPon: bSpeakerPhone)
         repeat {
-            API.turnSpeakerPhone(bSPon: bSpeakerPhone!)
+            Thread.sleep(forTimeInterval : 0.3)
             bAudioEngineIsAvtive = API.bAudioEngineIsRunning()
             print("Audio Session State: \(bAudioEngineIsAvtive ? "Active" : "Inactive" )")
             if (bAudioEngineIsAvtive) {
                 break
             }
+            API.restartAudioEngine()
         } while (!bAudioEngineIsAvtive)
         var volume:Float! = 0
         if (bSpeakerPhone) {
@@ -298,15 +298,14 @@ class QmeraAudioViewController: UIViewController {
 //    }
     
     override func viewWillDisappear(_ animated: Bool) {
+        NotificationCenter.default.removeObserver(self)
         UIDevice.current.isProximityMonitoringEnabled = false
         Nexilis.floatingButton.isHidden = false
-        Nexilis.callAPNActivated = false
     }
     
     deinit {
         UIDevice.current.isProximityMonitoringEnabled = false
         Nexilis.floatingButton.isHidden = false
-        Nexilis.callAPNActivated = false
         NotificationCenter.default.removeObserver(self)
         AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: "outputVolume")
     }
@@ -349,7 +348,7 @@ class QmeraAudioViewController: UIViewController {
         
         if isOutgoing {
             outgoingView()
-        } else if isOnGoing {
+        } else if isOnGoing || autoAcceptAPN {
             ongoingView()
         } else {
             incomingView()
@@ -373,12 +372,27 @@ class QmeraAudioViewController: UIViewController {
                     DispatchQueue.global().async {
                         if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getCalling(fPin: u.pin, type: "1"), timeout: 30 * 1000) {
                             if response.isOk() {
+                                DispatchQueue.main.async {
+                                    self.status.text = "Waiting for answer".localized()
+                                }
                             } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "01" {
                                 API.initiateCCall(sParty: u.pin)
                             } else {
                                 DispatchQueue.main.async {
-                                    self.status.text = "Busy"
+                                    Nexilis.stopRingbacktoneCall()
+                                }
+                                DispatchQueue.main.async {
+                                    let longCall =  "0"
+                                    Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Outgoing audio call".localized() + " at \(longCall)", fPin: User.getMyPin() ?? "", lPin: !self.data.isEmpty ? self.data : self.user != nil ? self.user!.pin : "", timeCall: self.timeStartCall, attachment_type: MessageScope.CALL)
+                                    self.status.text = "Busy..."
                                     self.end.isEnabled = false
+                                    if self.isOutgoing {
+                                        Nexilis.playBusyCall()
+                                    }
+                                    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                                        Nexilis.stopBusyCall()
+                                        self.didEnd(sender: false)
+                                    }
                                 }
                             }
                         } else {
@@ -403,17 +417,12 @@ class QmeraAudioViewController: UIViewController {
                     API.csa(sTicketID: ticketId, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: listRemoteViewFix, ivLocalView: cameraView, ivRemoteZ: zoomView, bCameraOn: false)
                 }
             } else if autoAcceptAPN {
-//                API.receiveCCall(sParty: u.pin)
-//                DispatchQueue.global().asyncAfter(deadline: .now() + 1, execute: {
-//                    CallManager.shared.endCall(with: APIS.uuidCall!)
                 DispatchQueue.global().async {
-                    if let response1 = Nexilis.writeSync(message: CoreMessage_TMessageBank.getNotifyCalling(fPin: u.pin, lPin: User.getMyPin()!, type: "1")) {
-                        if response1.isOk() {
-                //                            API.receiveCCall(sParty: u.pin)
-                        }
+                    while API.nGetCLXConnState() == 0 || !APIS.afterEnterForeground {
+                        Thread.sleep(forTimeInterval : 0.3)
                     }
+                    _ = Nexilis.write(message: CoreMessage_TMessageBank.getNotifyCalling(fPin: u.pin, lPin: User.getMyPin()!, type: "1"))
                 }
-//                })
             }
         }
         self.timeStartCall = String(Date().currentTimeMillis())
@@ -444,6 +453,23 @@ class QmeraAudioViewController: UIViewController {
                         API.initiateCCall(sParty: l_pin)
                     }
                 }
+            } else if data["call_cancel"] != nil {
+                DispatchQueue.main.async {
+                    Nexilis.stopRingbacktoneCall()
+                }
+                DispatchQueue.main.async {
+                    let longCall =  "0"
+                    Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Outgoing audio call".localized() + " at \(longCall)", fPin: User.getMyPin() ?? "", lPin: !self.data.isEmpty ? self.data : self.user != nil ? self.user!.pin : "", timeCall: self.timeStartCall, attachment_type: MessageScope.CALL)
+                    self.status.text = "Busy..."
+                    self.end.isEnabled = false
+                    if self.isOutgoing {
+                        Nexilis.playBusyCall()
+                    }
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                        Nexilis.stopBusyCall()
+                        self.didEnd(sender: false)
+                    }
+                }
             }
         }
     }
@@ -483,6 +509,34 @@ class QmeraAudioViewController: UIViewController {
         completion(user)
     }
     
+    private func resetViewToOutgoing() {
+        self.timer?.invalidate()
+        self.timer = nil
+        self.firstCall = false
+        status.removeFromSuperview()
+        profiles.removeFromSuperview()
+        name.removeFromSuperview()
+        stack.removeFromSuperview()
+        stack2.removeFromSuperview()
+        stackViewToolbar2.removeFromSuperview()
+        buttonWB.removeFromSuperview()
+        buttonChat.removeFromSuperview()
+        poweredByView.removeFromSuperview()
+        
+        view.addSubview(status)
+        view.addSubview(profiles)
+        view.addSubview(name)
+        
+        status.anchor(left: view.leftAnchor, bottom: profiles.topAnchor, right: view.rightAnchor, paddingBottom: 30, centerX: view.centerXAnchor)
+        profiles.anchor(centerX: view.centerXAnchor, centerY: view.centerYAnchor, width: 150, height: 150)
+        name.anchor(top: profiles.bottomAnchor, left: view.leftAnchor, right: view.rightAnchor, paddingTop: 5, paddingLeft: 20, paddingRight: 20, centerX: view.centerXAnchor)
+        status.text = "Connecting..."
+        view.addSubview(end)
+        end.anchor(bottom: view.bottomAnchor, paddingBottom: 60, centerX: view.centerXAnchor, width: buttonSize, height: buttonSize)
+        
+        end.addTarget(self, action: #selector(didPressEnd(sender:)), for: .touchUpInside)
+    }
+    
     private func outgoingView() {
 //        Nexilis.setSpeakerphoneOn(isSpeaker)
         if ticketId.isEmpty {
@@ -725,8 +779,6 @@ class QmeraAudioViewController: UIViewController {
                 }
             } else {
                 self.users.append(user)
-                // Start Calling
-//                Nexilis.shared.callManager.startCall(handle: user.pin)
                 if self.callFCM {
                     DispatchQueue.global().async {
                         if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getCalling(fPin: user.pin, type: "1"), timeout: 30 * 1000) {
@@ -756,11 +808,12 @@ class QmeraAudioViewController: UIViewController {
     }
     
     @objc func didPressEnd(sender: Any?) {
-        Nexilis.stopRingtoneCall()
-        Nexilis.stopRingbacktoneCall()
+        if let sharedAudioPlayer = Nexilis.sharedAudioPlayer, sharedAudioPlayer.isPlaying {
+            Nexilis.stopRingtoneCall()
+            Nexilis.stopRingbacktoneCall()
+        }
         let onGoingCC: String = SecureUserDefaults.shared.value(forKey: "onGoingCC") ?? ""
         if !onGoingCC.isEmpty {
-            self.isEndByMe = true
             self.didEnd(sender: nil)
             return
         }
@@ -774,6 +827,17 @@ class QmeraAudioViewController: UIViewController {
                 } else if !self.isOutgoing {
                     Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Incoming audio call".localized() + " at \(self.status.text ?? "")", fPin: !self.data.isEmpty ? self.data : self.user != nil ? self.user!.pin : "", lPin: User.getMyPin() ?? "", timeCall: self.timeStartCall, attachment_type: MessageScope.CALL)
                 }
+                if self.callFCM && self.timer == nil {
+                    DispatchQueue.global().async {
+                        if let _ = Nexilis.writeSync(message: CoreMessage_TMessageBank.getCancelCall(fPin: self.user!.pin, type: "1"), timeout: 30 * 1000) {
+                        } else {
+                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                            banner.show()
+                        }
+                    }
+                }
                 self.timer?.invalidate()
                 self.timer = nil
                 self.status.text = "Audio Call Ended".localized()
@@ -782,15 +846,16 @@ class QmeraAudioViewController: UIViewController {
                 self.speaker.isEnabled = false
                 self.mic.isEnabled = false
             }
-            self.isEndByMe = true
             self.didEnd(sender: nil)
         }))
         self.present(alert, animated: true, completion: nil)
     }
     
     @objc func didEnd(sender: Any?) {
-        Nexilis.stopRingtoneCall()
-        Nexilis.stopRingbacktoneCall()
+        if let sharedAudioPlayer = Nexilis.sharedAudioPlayer, sharedAudioPlayer.isPlaying {
+            Nexilis.stopRingtoneCall()
+            Nexilis.stopRingbacktoneCall()
+        }
         poweredByView.isHidden = true
         let onGoingCC: String = SecureUserDefaults.shared.value(forKey: "onGoingCC") ?? ""
         if !onGoingCC.isEmpty {
@@ -876,11 +941,7 @@ class QmeraAudioViewController: UIViewController {
                     SecureUserDefaults.shared.removeValue(forKey: "startTimeCC")
                     SecureUserDefaults.shared.removeValue(forKey: "waitingRequestCC")
                 }
-//                if let user = self.user, let call = Nexilis.shared.callManager.call(with: user.pin) {
-//                    Nexilis.shared.callManager.end(call: call)
-//                } else {
-                    API.terminateCall(sParty: nil)
-//                }
+                API.terminateCall(sParty: nil)
             }))
             self.present(alert, animated: true, completion: nil)
         } else {
@@ -888,31 +949,8 @@ class QmeraAudioViewController: UIViewController {
             if controller != nil {
                 controller!.dismiss(animated: true)
             }
-            if isEndByMe {
-//                for i in 0..<Nexilis.shared.callManager.calls.count {
-//                    Nexilis.shared.callManager.end(call: Nexilis.shared.callManager.calls[i])
-//                }
-                if callFCM && self.timer == nil {
-                    DispatchQueue.global().async {
-                        if let _ = Nexilis.writeSync(message: CoreMessage_TMessageBank.getCancelCall(fPin: self.user!.pin, type: "1"), timeout: 30 * 1000) {
-                        } else {
-                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
-                            imageView.tintColor = .white
-                            let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
-                            banner.show()
-                        }
-                    }
-                }
-                API.terminateCall(sParty: nil)
-                self.dismiss(animated: false, completion: nil)
-            } else {
-//                if let user = self.user, let call = Nexilis.shared.callManager.call(with: user.pin) {
-//                    Nexilis.shared.callManager.end(call: call)
-//                } else {
-                    API.terminateCall(sParty: nil)
-//                }
-                self.dismiss(animated: false, completion: nil)
-            }
+            API.terminateCall(sParty: nil)
+            self.dismiss(animated: false, completion: nil)
         }
     }
     
@@ -973,7 +1011,12 @@ class QmeraAudioViewController: UIViewController {
            let message = data["message"] as? String
         {
             let arrayMessage = message.split(separator: ",")
-            if state == Nexilis.AUDIO_VIDEO_CALL_MUTED {
+            if state == Nexilis.AUDIO_CALL_INCOMING {
+                if autoAcceptAPN {
+                    API.receiveCCall(sParty: self.user?.pin)
+                }
+            }
+            else if state == Nexilis.AUDIO_VIDEO_CALL_MUTED {
                 DispatchQueue.main.async { [self] in
                     if let pin = arrayMessage.first, let index = users.firstIndex(of: User(pin: String(pin))) {
                         if arrayMessage[1] == "1" {
@@ -990,32 +1033,17 @@ class QmeraAudioViewController: UIViewController {
                     }
                 }
             } else if state == Nexilis.STREAMING_SEMINAR_ENDED { // always call turnspeaker
-                QmeraAudioViewController.isLoop = true
-                DispatchQueue.global(qos: .userInitiated).async {
-                    repeat {
-                        Thread.sleep(forTimeInterval : 1)
-                        if (QmeraAudioViewController.isLoop && !API.bAudioEngineIsRunning()) {
-                            API.turnSpeakerPhone(bSPon: QmeraAudioViewController.bSpeakerPhone!)
-                        }
-                    } while (QmeraAudioViewController.isLoop)
-                }
-//                DispatchQueue.global().asyncAfter(deadline: .now() + 3, execute: {
-//                    API.turnSpeakerPhone(bSPon: QmeraAudioViewController.bSpeakerPhone!)
-//                })
-//                DispatchQueue.global().async {
-//                    var bAudioSessionIsAvtive: Bool! = false
+//                QmeraAudioViewController.isLoop = true
+//                DispatchQueue.global(qos: .userInitiated).async {
 //                    repeat {
-//                        API.turnSpeakerPhone(bSPon: QmeraAudioViewController.bSpeakerPhone!)
-//                        let audioSession = AVAudioSession.sharedInstance()
-//                        bAudioSessionIsAvtive = !audioSession.secondaryAudioShouldBeSilencedHint
-//                        print("repeat turnSpeakerPhone >> \(bAudioSessionIsAvtive)")
-//                        if (bAudioSessionIsAvtive) {
-//                            break
+//                        Thread.sleep(forTimeInterval : 1)
+//                        if (QmeraAudioViewController.isLoop && !API.bAudioEngineIsRunning()) {
+//                            API.turnSpeakerPhone(bSPon: QmeraAudioViewController.bSpeakerPhone!)
 //                        }
-//                    } while (!bAudioSessionIsAvtive)
+//                    } while (QmeraAudioViewController.isLoop)
 //                }
             } else if state == Nexilis.AUDIO_CALL_RINGING || (!ticketId.isEmpty && state == Nexilis.VIDEO_CALL_RINGING) {
-                if users.count == 1 {
+                if users.count == 1 && !autoAcceptAPN {
                     DispatchQueue.main.async {
                         self.status.text = "Waiting for answer".localized()
                     }
@@ -1048,10 +1076,37 @@ class QmeraAudioViewController: UIViewController {
                 if (!isOutgoing || !firstCall), users.count >= 1, let user = User.getData(pin: String(arrayMessage[1])), !users.contains(user) {
                     self.users.append(user)
                 }
+                users.forEach({ $0.isConnected = true })
+            } else if state == Nexilis.OUTGOING_CALL {
+                DispatchQueue.main.async {
+                    if self.timer == nil && !self.stack.isDescendant(of: self.view) {
+                        DispatchQueue.main.async {
+                            if !self.ticketId.isEmpty {
+                                NSLayoutConstraint.deactivate(self.stack.constraints)
+                                self.stack.subviews.forEach { subview in
+                                    subview.removeFromSuperview()
+                                }
+                                UIView.animate(withDuration: 0.3, animations: {
+                                    self.view.layoutIfNeeded()
+                                })
+                            }
+                            self.ongoingView()
+                            let connectDate = Date()
+                            self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
+                                let format = Utils.callDurationFormatter.string(from: Date().timeIntervalSince(connectDate))
+                                self.status.text = format
+                            }
+                            self.timer?.fire()
+                            self.firstCall = false
+                        }
+                    }
+                }
             } else if state == Nexilis.AUDIO_CALL_END || (!ticketId.isEmpty && state == Nexilis.VIDEO_CALL_END) {
                 DispatchQueue.main.async {
-                    Nexilis.stopRingtoneCall()
-                    Nexilis.stopRingbacktoneCall()
+                    if let sharedAudioPlayer = Nexilis.sharedAudioPlayer, sharedAudioPlayer.isPlaying {
+                        Nexilis.stopRingtoneCall()
+                        Nexilis.stopRingbacktoneCall()
+                    }
                 }
                 let onGoingCC: String = SecureUserDefaults.shared.value(forKey: "onGoingCC") ?? ""
                 if let pin = arrayMessage.first, let index = users.firstIndex(of: User(pin: String(pin))) {
@@ -1083,6 +1138,8 @@ class QmeraAudioViewController: UIViewController {
                             }
                             QmeraAudioViewController.isLoop = false
                             QmeraAudioViewController.bSpeakerPhone = false
+                            do { try AVAudioSession.sharedInstance().setActive(false) } catch {}
+                            Nexilis.callAPNActivated = false
                             DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                                 self.didEnd(sender: true)
                             }
@@ -1112,6 +1169,8 @@ class QmeraAudioViewController: UIViewController {
                         }
                         QmeraAudioViewController.isLoop = false
                         QmeraAudioViewController.bSpeakerPhone = false
+                        do { try AVAudioSession.sharedInstance().setActive(false) } catch {}
+                        Nexilis.callAPNActivated = false
                         DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                             self.didEnd(sender: true)
                         }
@@ -1142,10 +1201,18 @@ class QmeraAudioViewController: UIViewController {
                         }
                         QmeraAudioViewController.isLoop = false
                         QmeraAudioViewController.bSpeakerPhone = false
+                        do { try AVAudioSession.sharedInstance().setActive(false) } catch {}
+                        Nexilis.callAPNActivated = false
                         DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                             self.didEnd(sender: true)
                         }
                         return
+                    } else if users.count == 1 {
+                        if !users[0].isConnected{
+                            DispatchQueue.main.async {
+                                self.resetViewToOutgoing()
+                            }
+                        }
                     }
                     DispatchQueue.main.async{ [self] in
                         if users.count == 1 && !buttonWB.isEnabled {
@@ -1190,10 +1257,6 @@ class QmeraAudioViewController: UIViewController {
                     }
                 }
             } else if state == Nexilis.BUSY { // Busy
-                DispatchQueue.main.async {
-                    Nexilis.stopRingtoneCall()
-                    Nexilis.stopRingbacktoneCall()
-                }
                 let onGoingCC: String = SecureUserDefaults.shared.value(forKey: "onGoingCC") ?? ""
                 if let pin = arrayMessage.first, let index = users.firstIndex(of: User(pin: String(pin))) {
                     users.remove(at: index)
@@ -1223,13 +1286,17 @@ class QmeraAudioViewController: UIViewController {
                         self.status.text = "Busy..."
                         self.end.isEnabled = false
                         if self.isOutgoing {
-                            Nexilis.playBusyCall()   
+                            Nexilis.playBusyCall()
                         }
                         DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
-                            Nexilis.stopBusyCall()
                             self.didEnd(sender: false)
                         }
                     }
+                } else {
+                    DispatchQueue.main.async {
+                        Nexilis.stopRingtoneCall()
+                        Nexilis.stopRingbacktoneCall()
+                    }
                 }
             }
         }

+ 109 - 56
NexilisLite/NexilisLite/Source/View/Call/QmeraVideoViewController.swift

@@ -18,8 +18,8 @@ import MediaPlayer
 
 class QmeraVideoViewController: UIViewController {
     
-    static private let nMaxSPOn: Float! = 10.0
-    static private let nMaxSPOff: Float! = 9.0
+    static private var nMaxSPOn: Float! = 20.0
+    static private var nMaxSPOff: Float! = 20.0
     static private var volumeView: MPVolumeView!
     static private var lastVolume: Float! = AVAudioSession.sharedInstance().outputVolume
     static private var bSpeakerPhone: Bool! = false
@@ -146,13 +146,15 @@ class QmeraVideoViewController: UIViewController {
     static func turnSpeakerOn() {
         bSpeakerPhone = !bSpeakerPhone
         var bAudioEngineIsAvtive: Bool! = false
+        API.turnSpeakerPhone(bSPon: bSpeakerPhone)
         repeat {
-            API.turnSpeakerPhone(bSPon: bSpeakerPhone!)
+            Thread.sleep(forTimeInterval : 0.3)
             bAudioEngineIsAvtive = API.bAudioEngineIsRunning()
             print("Audio Session State: \(bAudioEngineIsAvtive ? "Active" : "Inactive" )")
             if (bAudioEngineIsAvtive) {
                 break
             }
+            API.restartAudioEngine()
         } while (!bAudioEngineIsAvtive)
         var volume:Float! = 0
         if (bSpeakerPhone) {
@@ -182,7 +184,6 @@ class QmeraVideoViewController: UIViewController {
         navigationController?.interactivePopGestureRecognizer?.isEnabled = true
         NotificationCenter.default.removeObserver(self)
         Nexilis.floatingButton.isHidden = false
-        Nexilis.callAPNActivated = false
     }
     
     override func viewWillDisappear(_ animated: Bool) {
@@ -195,7 +196,6 @@ class QmeraVideoViewController: UIViewController {
             NotificationCenter.default.removeObserver(self)
         }
         Nexilis.floatingButton.isHidden = false
-        Nexilis.callAPNActivated = false
     }
     
     private func backToDefaultAudioSession() {
@@ -242,12 +242,11 @@ class QmeraVideoViewController: UIViewController {
             didTapAcceptCallButton()
         }
         if autoAcceptAPN {
-//            API.receiveCCall(sParty: dataPerson[0]["f_pin"]!, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: listRemoteViewFix, ivLocalView: cameraView,ivRemoteZ: zoomView)
             DispatchQueue.global().async {
-                if let response1 = Nexilis.writeSync(message: CoreMessage_TMessageBank.getNotifyCalling(fPin: self.fPin, lPin: User.getMyPin()!, type: "1")) {
-                    if response1.isOk() {
-                    }
+                while API.nGetCLXConnState() == 0 || !APIS.afterEnterForeground {
+                    Thread.sleep(forTimeInterval : 0.3)
                 }
+                _ = Nexilis.writeSync(message: CoreMessage_TMessageBank.getNotifyCalling(fPin: self.fPin, lPin: User.getMyPin()!, type: "2"))
             }
         }
         self.timeStartCall = String(Date().currentTimeMillis())
@@ -485,7 +484,9 @@ class QmeraVideoViewController: UIViewController {
                     DispatchQueue.global().async {
                         if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getCalling(fPin: self.dataPerson[0]["f_pin"]!!, type: "2"), timeout: 30 * 1000) {
                             if response.isOk() {
-                                
+                                DispatchQueue.main.async {
+                                    self.labelIncomingOutgoing.text = "Waiting for answer".localized()
+                                }
                             } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "01" && self.dataPerson.count > 0 {
                                 API.initiateCCall(sParty: self.dataPerson[0]["f_pin"]!, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: self.listRemoteViewFix, ivLocalView: self.cameraView, ivRemoteZ: self.zoomView)
                             } else {
@@ -520,7 +521,7 @@ class QmeraVideoViewController: UIViewController {
                 }
             }
         } else {
-            if ticketId.isEmpty {
+            if ticketId.isEmpty && !autoAcceptAPN {
                 backToDefaultAudioSession()
                 Nexilis.playRingtoneCall()
             }
@@ -533,11 +534,11 @@ class QmeraVideoViewController: UIViewController {
         labelIncomingOutgoing.textColor = .mainColor
     }
     
-    func addToolbar() {
+    func addToolbar(resetToolbar: Bool = false) {
         view.addSubview(buttonDecline)
         buttonDecline.translatesAutoresizingMaskIntoConstraints = false
         buttonDecline.frame.size = CGSize(width: 70.0, height: 70.0)
-        if isInisiator {
+        if isInisiator || resetToolbar {
             constraintLeadingButtonDecline = buttonDecline.centerXAnchor.constraint(equalTo: view.centerXAnchor)
         } else {
             constraintLeadingButtonDecline = buttonDecline.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: view.frame.width * 0.2)
@@ -555,7 +556,7 @@ class QmeraVideoViewController: UIViewController {
         buttonDecline.tintColor = .white
         buttonDecline.addTarget(self, action: #selector(didTapDeclineCallButton(sender:)), for: .touchUpInside)
         
-        if !isInisiator{
+        if !isInisiator && !resetToolbar {
             view.addSubview(buttonAccept)
             buttonAccept.translatesAutoresizingMaskIntoConstraints = false
             buttonAccept.frame.size = CGSize(width: 70.0, height: 70.0)
@@ -579,7 +580,9 @@ class QmeraVideoViewController: UIViewController {
             if let l_pin = data["l_pin"] as? String {
                 if let f_pin = data["f_pin"] as? String {
                     if f_pin == User.getMyPin()!  {
-                        labelIncomingOutgoing.text = "Waiting for answer".localized()
+                        if !Nexilis.callAPNActivated {
+                            labelIncomingOutgoing.text = "Waiting for answer".localized()
+                        }
                         API.initiateCCall(sParty: l_pin, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: listRemoteViewFix, ivLocalView: cameraView, ivRemoteZ: zoomView)
                     }
                 }
@@ -648,6 +651,8 @@ class QmeraVideoViewController: UIViewController {
                 Nexilis.stopRingbacktoneCall()
                 QmeraVideoViewController.isLoop = false
                 QmeraVideoViewController.bSpeakerPhone = false
+                do { try AVAudioSession.sharedInstance().setActive(false) } catch {}
+                Nexilis.callAPNActivated = false
                 self.endAllCall()
                 self.dismiss(animated: true, completion: nil)
             }))
@@ -705,6 +710,8 @@ class QmeraVideoViewController: UIViewController {
 //                self.labelTimerVC.text = "Video call is over".localized()
                 QmeraVideoViewController.isLoop = false
                 QmeraVideoViewController.bSpeakerPhone = false
+                do { try AVAudioSession.sharedInstance().setActive(false) } catch {}
+                Nexilis.callAPNActivated = false
                 self.endAllCall()
                 DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                     if self.isInisiator && !self.isPresent {
@@ -771,11 +778,46 @@ class QmeraVideoViewController: UIViewController {
                     self.labelTimerVC.text = format
                 }
                 self.vcTimer.fire()
-//                self.setSpeaker()
+                if !QmeraVideoViewController.bSpeakerPhone {
+                    self.setSpeaker()
+                }
             }
         }
     }
     
+    private func resetViewToOutgoing() {
+        self.vcTimer.invalidate()
+        self.zoomView.image = nil
+        self.cameraView.image = nil
+        if self.stackViewToolbar.isDescendant(of: self.view){
+            self.stackViewToolbar.removeFromSuperview()
+        }
+        if self.stackViewToolbar2.isDescendant(of: self.view){
+            self.stackViewToolbar2.removeFromSuperview()
+        }
+        if self.buttonWB.isDescendant(of: self.view){
+            self.buttonWB.removeFromSuperview()
+        }
+        if self.buttonChat.isDescendant(of: self.view){
+            self.buttonChat.removeFromSuperview()
+        }
+        if self.buttonDecline.isDescendant(of: self.view) {
+            self.buttonDecline.removeFromSuperview()
+        }
+        if self.buttonAccept.isDescendant(of: self.view) {
+            self.buttonAccept.removeFromSuperview()
+        }
+        if self.buttonRotate.isDescendant(of: self.view) {
+            self.buttonRotate.removeFromSuperview()
+        }
+        if self.buttonMuted.isDescendant(of: self.view) {
+            self.buttonMuted.removeFromSuperview()
+        }
+        addBackgroundIncoming()
+        addProfileNameCalling()
+        addToolbar(resetToolbar: true)
+    }
+    
     @objc func didTapChatButton(){
         let onGoingCC: String = SecureUserDefaults.shared.value(forKey: "onGoingCC") ?? ""
         let members: String = SecureUserDefaults.shared.value(forKey: "membersCC") ?? ""
@@ -1112,6 +1154,9 @@ class QmeraVideoViewController: UIViewController {
                                 if response.isOk() {
                                 } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "01" {
                                     self.dataPerson.append(data)
+                                    if let user = User.getData(pin: String(data["f_pin"]!!)) {
+                                        self.users.append(user)
+                                    }
                                     API.initiateCCall(sParty: data["f_pin"]!, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: self.listRemoteViewFix, ivLocalView: self.cameraView, ivRemoteZ: self.zoomView)
                                     Nexilis.playRingbacktoneCall()
                                 } else {
@@ -1127,6 +1172,9 @@ class QmeraVideoViewController: UIViewController {
                     } else {
                         DispatchQueue.main.async {
                             self.dataPerson.append(data)
+                            if let user = User.getData(pin: String(data["f_pin"]!!)) {
+                                self.users.append(user)
+                            }
                             API.initiateCCall(sParty: data["f_pin"]!, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: self.listRemoteViewFix, ivLocalView: self.cameraView, ivRemoteZ: self.zoomView)
                             Nexilis.playRingbacktoneCall()
                         }
@@ -1191,7 +1239,12 @@ class QmeraVideoViewController: UIViewController {
         let message = (data?["message"] ?? "") as! String
         var remoteChannel = [String:String]()
         let arrayMessage = message.split(separator: ",")
-        if state == Nexilis.AUDIO_VIDEO_CALL_MUTED {
+        if state == Nexilis.VIDEO_CALL_INCOMING {
+            if autoAcceptAPN {
+                didTapAcceptCallButton()
+            }
+        }
+        else if state == Nexilis.AUDIO_VIDEO_CALL_MUTED {
             DispatchQueue.main.async { [self] in
                 if self.dataPerson.count == 1 {
                     if arrayMessage[1] == "1" {
@@ -1203,7 +1256,7 @@ class QmeraVideoViewController: UIViewController {
             }
         } else if (state == Nexilis.VIDEO_CALL_ZOOM) && self.dataPerson.count > 1 {
             DispatchQueue.main.async {
-                if arrayMessage[2] != "2" {
+                if arrayMessage[0] == arrayMessage[3] {
                     self.zoomView.transform   = CGAffineTransform.init(scaleX: -1.9, y: 2.2).rotated(by: (-CGFloat.pi)/2)
                     self.zoomView.contentMode = .scaleAspectFit
                 } else {
@@ -1223,35 +1276,19 @@ class QmeraVideoViewController: UIViewController {
             }
         }
         else if state == Nexilis.STREAMING_SEMINAR_ENDED { // always call turnspeaker
-            QmeraVideoViewController.isLoop = true
-            DispatchQueue.global().asyncAfter(deadline: .now() + 0.5, execute: {
-                API.adjustVolume(fValue: 10.0)
-                self.setSpeaker()
-                DispatchQueue.global(qos: .userInitiated).async {
-                    repeat {
-                        Thread.sleep(forTimeInterval : 1)
-                        print("Audio: \(API.bAudioEngineIsRunning())")
-                        if (QmeraVideoViewController.isLoop && !API.bAudioEngineIsRunning()) {
-                            API.turnSpeakerPhone(bSPon: QmeraVideoViewController.bSpeakerPhone!)
-                        }
-                    } while (QmeraVideoViewController.isLoop)
-                }
-            })
-//                DispatchQueue.global().asyncAfter(deadline: .now() + 3, execute: {
-//                    API.turnSpeakerPhone(bSPon: QmeraAudioViewController.bSpeakerPhone!)
-//                })
-//                DispatchQueue.global().async {
-//                    var bAudioSessionIsAvtive: Bool! = false
+//            QmeraVideoViewController.isLoop = true
+//            DispatchQueue.global().asyncAfter(deadline: .now() + 0.5, execute: {
+//                API.adjustVolume(fValue: 10.0)
+//                self.setSpeaker()
+//                DispatchQueue.global(qos: .userInitiated).async {
 //                    repeat {
-//                        API.turnSpeakerPhone(bSPon: QmeraAudioViewController.bSpeakerPhone!)
-//                        let audioSession = AVAudioSession.sharedInstance()
-//                        bAudioSessionIsAvtive = !audioSession.secondaryAudioShouldBeSilencedHint
-//                        print("repeat turnSpeakerPhone >> \(bAudioSessionIsAvtive)")
-//                        if (bAudioSessionIsAvtive) {
-//                            break
+//                        Thread.sleep(forTimeInterval : 1)
+//                        if (QmeraVideoViewController.isLoop && !API.bAudioEngineIsRunning()) {
+//                            API.turnSpeakerPhone(bSPon: QmeraVideoViewController.bSpeakerPhone!)
 //                        }
-//                    } while (!bAudioSessionIsAvtive)
+//                    } while (QmeraVideoViewController.isLoop)
 //                }
+//            })
         }
         else if (state == Nexilis.VIDEO_CALL_OFFHOOK) {
             DispatchQueue.main.async {
@@ -1353,8 +1390,13 @@ class QmeraVideoViewController: UIViewController {
                     }
                 }
                 
-                if self.users.count >= 1, let user = User.getData(pin: String(arrayMessage[1])), !self.users.contains(user) {
-                    self.users.append(user)
+                if let user = User.getData(pin: String(arrayMessage[1])) {
+                    if !self.users.contains(user) {
+                        user.isConnected = true
+                        self.users.append(user)
+                    } else if let userEx = self.users.firstIndex(where: { $0.pin == String(arrayMessage[1]) }) {
+                        self.users[userEx].isConnected = true
+                    }
                 }
                 if arrayMessage[5] == "2" && self.dataPerson.count == 1 {
                     DispatchQueue.main.async {
@@ -1381,7 +1423,7 @@ class QmeraVideoViewController: UIViewController {
                 }
             }
             DispatchQueue.main.async {
-                if self.isInisiator && self.name.isDescendant(of: self.view) {
+                if self.name.isDescendant(of: self.view) {
                     self.didTapAcceptCallButton()
                 }
                 let indexPerson = self.dataPerson.firstIndex(where: {$0["f_pin"]!! == arrayMessage[1]})
@@ -1443,6 +1485,8 @@ class QmeraVideoViewController: UIViewController {
                     }
                     QmeraVideoViewController.isLoop = false
                     QmeraVideoViewController.bSpeakerPhone = false
+                    do { try AVAudioSession.sharedInstance().setActive(false) } catch {}
+                    Nexilis.callAPNActivated = false
                     DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                         self.endAllCall()
                         self.dismiss(animated: true, completion: nil)
@@ -1452,10 +1496,6 @@ class QmeraVideoViewController: UIViewController {
             }
             DispatchQueue.main.async {
                 if (self.dataPerson.count == 1) {
-                    var longCall = "0"
-                    if self.vcTimer.isValid {
-                        longCall = self.labelTimerVC.text ?? ""
-                    }
                     self.makeStateCall()
 //                    if self.labelIncomingOutgoing.isDescendant(of: self.view) {
 //                        self.labelIncomingOutgoing.text = "Video call is over".localized()
@@ -1497,6 +1537,8 @@ class QmeraVideoViewController: UIViewController {
                     }
                     QmeraVideoViewController.isLoop = false
                     QmeraVideoViewController.bSpeakerPhone = false
+                    do { try AVAudioSession.sharedInstance().setActive(false) } catch {}
+                    Nexilis.callAPNActivated = false
                     DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                         self.endAllCall()
                         if self.isInisiator && !self.isPresent {
@@ -1539,15 +1581,18 @@ class QmeraVideoViewController: UIViewController {
                         }
                         self.dataPerson.remove(at: indexPerson!)
                     }
-                    if !onGoingCC.isEmpty {
-                        if let pin = arrayMessage.first, let index = self.users.firstIndex(of: User(pin: String(pin))) {
-                            self.users.remove(at: index)
-                        }
+                    let pin = "\(arrayMessage[0])"
+                    if let index = self.users.firstIndex(where: { $0.pin == pin }) {
+                        self.users.remove(at: index)
                     }
                     
                     if self.dataPerson.count == 1 {
                         self.transformZoomAfterNewUserMore2 = false
                         self.zoomView.transform   = CGAffineTransform.init(scaleX: 1.9, y: 2.2).rotated(by: (CGFloat.pi)/2)
+                        
+                        if !self.users[0].isConnected {
+                            self.resetViewToOutgoing()
+                        }
                     }
                 }
             }
@@ -1691,7 +1736,9 @@ class QmeraVideoViewController: UIViewController {
                             }
                         }
                     }
-                    self.dataPerson.remove(at: indexPerson!)
+                    if let indexP = indexPerson {
+                        self.dataPerson.remove(at: indexP)
+                    }
                 }
             }
             if (self.dataPerson.count == 1) {
@@ -1773,6 +1820,12 @@ class QmeraVideoViewController: UIViewController {
                     if self.buttonRotate.isEnabled {
                         self.buttonRotate.isEnabled = false
                     }
+                } else {
+                    if self.dataPerson.count == 1 && !self.autoAcceptAPN {
+                        DispatchQueue.main.async {
+                            self.labelIncomingOutgoing.text = "Waiting for answer".localized()
+                        }
+                    }
                 }
             }
         }

+ 127 - 42
NexilisLite/NexilisLite/Source/View/Chat/ChatWALikeVC.swift

@@ -15,6 +15,7 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
     private let searchController = UISearchController(searchResultsController: nil)
     
     var chats: [Chat] = []
+    var tempChats: [Chat] = []
     var chatGroupMaps: [String: [Chat]] = [:]
     var loadingData = true
     private let textChatEmpty = UILabel()
@@ -26,6 +27,8 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
     let contGroups = UIButton(type: .custom)
     let textGroups = UILabel()
     
+    private static var filterMain = 0
+    
     var fillteredData: [Any] = []
     var fillteredMessages: [Chat] = []
     
@@ -67,10 +70,15 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
         tabBarController?.navigationController?.setNavigationBarHidden(true, animated: false)
         navigationController?.navigationBar.prefersLargeTitles = true
         navigationController?.navigationItem.largeTitleDisplayMode = .always
+        navigationController?.navigationBar.backgroundColor = .clear
+        navigationController?.navigationBar.tintColor = .black
+        navigationController?.navigationBar.overrideUserInterfaceStyle = .light
+        self.setNeedsStatusBarAppearanceUpdate()
         let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.foregroundColor: self.traitCollection.userInterfaceStyle == .dark ? .white : UIColor.black, NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 16)]
         let largeAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.foregroundColor: self.traitCollection.userInterfaceStyle == .dark ? .white : UIColor.black, NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 34)]
         let appearance = UINavigationBarAppearance()
         appearance.configureWithTransparentBackground()
+        appearance.backgroundColor = .clear
         appearance.titleTextAttributes = attributes
         appearance.largeTitleTextAttributes = largeAttributes
         navigationController?.navigationBar.standardAppearance = appearance
@@ -118,9 +126,12 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
     
     private func refresh() {
         getData { [self] in
-            if chats.count == 0 {
-                searchController.searchBar.isHidden = true
-                if textChatEmpty.isDescendant(of: view){
+            if tempChats.count == 0 {
+                navigationItem.searchController = nil
+                DispatchQueue.main.async {
+                    self.navigationController?.navigationBar.sizeToFit()
+                }
+                if textChatEmpty.isDescendant(of: view) {
                     textChatEmpty.removeFromSuperview()
                 }
                 textChatEmpty.numberOfLines = 0
@@ -133,15 +144,21 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
                 }
                 textChatEmpty.attributedText = attributedString
                 
-                view.addSubview(textChatEmpty)
-                textChatEmpty.anchor(left: view.leftAnchor, right: view.rightAnchor, paddingLeft: 20, paddingRight: 20, centerX: view.centerXAnchor, centerY: view.centerYAnchor)
+                tableView.addSubview(textChatEmpty)
+                textChatEmpty.anchor(left: tableView.leftAnchor, right: tableView.rightAnchor, paddingLeft: 20, paddingRight: 20, centerX: tableView.centerXAnchor)
+                textChatEmpty.centerYAnchor.constraint(equalTo: tableView.centerYAnchor, constant: -60).isActive = true
             } else {
-                searchController.searchBar.isHidden = false
+                if textChatEmpty.isDescendant(of: view) {
+                    textChatEmpty.removeFromSuperview()
+                }
             }
         }
     }
     
     public override func viewDidAppear(_ animated: Bool) {
+        if Nexilis.floatingButton.isHidden {
+            Nexilis.floatingButton.isHidden = false
+        }
         refresh()
     }
     
@@ -198,8 +215,14 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
                     }
                 }
             }
-
-            self.chats = tempChats
+            self.tempChats = tempChats
+            if ChatWALikeVC.filterMain == 0 {
+                self.chats = tempChats
+            } else if ChatWALikeVC.filterMain == 1 {
+                self.chats = tempChats.filter({ Int($0.counter) ?? 0 > 0 })
+            } else {
+                self.chats = tempChats.filter({ !$0.groupId.isEmpty })
+            }
             completion()
         }
     }
@@ -227,19 +250,19 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
     public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
         let header = UIView()
         
-        if chats.count > 0 {
+        if tempChats.count > 0 {
             header.addSubview(contAll)
             contAll.anchor(top: header.topAnchor, left: header.leftAnchor, bottom: header.bottomAnchor, paddingLeft: 20, height: 30)
             contAll.layer.cornerRadius = 15
             contAll.clipsToBounds = true
             contAll.tag = 0
-            contAll.backgroundColor = .whatsappGreenLightColor
+            contAll.backgroundColor = ChatWALikeVC.filterMain == 0 ? .whatsappGreenLightColor : .whiteBubbleColor
             contAll.addTarget(self, action: #selector(selectFilter), for: .touchUpInside)
             
             contAll.addSubview(textAll)
             textAll.text = "All".localized()
             textAll.font = .boldSystemFont(ofSize: 14)
-            textAll.textColor = .whatsappGreenTitleColor
+            textAll.textColor = ChatWALikeVC.filterMain == 0 ? .whatsappGreenTitleColor : .grayTitleColor
             textAll.anchor(left: contAll.leftAnchor, right: contAll.rightAnchor, paddingLeft: 15, paddingRight: 15, centerX: contAll.centerXAnchor, centerY: contAll.centerYAnchor, height: 20)
             
             header.addSubview(contUnread)
@@ -247,13 +270,13 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
             contUnread.layer.cornerRadius = 15
             contUnread.clipsToBounds = true
             contUnread.tag = 1
-            contUnread.backgroundColor = .whiteBubbleColor
+            contUnread.backgroundColor = ChatWALikeVC.filterMain == 1 ? .whatsappGreenLightColor : .whiteBubbleColor
             contUnread.addTarget(self, action: #selector(selectFilter), for: .touchUpInside)
             
             contUnread.addSubview(textUnread)
             textUnread.text = "Unread".localized()
             textUnread.font = .boldSystemFont(ofSize: 14)
-            textUnread.textColor = .grayTitleColor
+            textUnread.textColor = ChatWALikeVC.filterMain == 1 ? .whatsappGreenTitleColor : .grayTitleColor
             textUnread.anchor(left: contUnread.leftAnchor, right: contUnread.rightAnchor, paddingLeft: 15, paddingRight: 15, centerX: contUnread.centerXAnchor, centerY: contUnread.centerYAnchor, height: 20)
             
             header.addSubview(contGroups)
@@ -261,13 +284,13 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
             contGroups.layer.cornerRadius = 15
             contGroups.clipsToBounds = true
             contGroups.tag = 2
-            contGroups.backgroundColor = .whiteBubbleColor
+            contGroups.backgroundColor = ChatWALikeVC.filterMain == 2 ? .whatsappGreenLightColor : .whiteBubbleColor
             contGroups.addTarget(self, action: #selector(selectFilter), for: .touchUpInside)
             
             contGroups.addSubview(textGroups)
             textGroups.text = "Groups".localized()
             textGroups.font = .boldSystemFont(ofSize: 14)
-            textGroups.textColor = .grayTitleColor
+            textGroups.textColor = ChatWALikeVC.filterMain == 2 ? .whatsappGreenTitleColor : .grayTitleColor
             textGroups.anchor(left: contGroups.leftAnchor, right: contGroups.rightAnchor, paddingLeft: 15, paddingRight: 15, centerX: contGroups.centerXAnchor, centerY: contGroups.centerYAnchor, height: 20)
         }
         
@@ -275,33 +298,18 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
     }
     
     @objc func selectFilter(_ sender: UIButton) {
-        if sender.tag == 0 && contAll.backgroundColor != .whatsappGreenLightColor {
-            contAll.backgroundColor = .whatsappGreenLightColor
-            textAll.textColor = .whatsappGreenTitleColor
-            
-            contUnread.backgroundColor = .whiteBubbleColor
-            textUnread.textColor = .grayTitleColor
-            
-            contGroups.backgroundColor = .whiteBubbleColor
-            textGroups.textColor = .grayTitleColor
-        } else if sender.tag == 1 && contUnread.backgroundColor != .whatsappGreenLightColor {
-            contUnread.backgroundColor = .whatsappGreenLightColor
-            textUnread.textColor = .whatsappGreenTitleColor
-            
-            contAll.backgroundColor = .whiteBubbleColor
-            textAll.textColor = .grayTitleColor
-            
-            contGroups.backgroundColor = .whiteBubbleColor
-            textGroups.textColor = .grayTitleColor
-        }  else if sender.tag == 2 && contGroups.backgroundColor != .whatsappGreenLightColor {
-            contGroups.backgroundColor = .whatsappGreenLightColor
-            textGroups.textColor = .whatsappGreenTitleColor
-            
-            contAll.backgroundColor = .whiteBubbleColor
-            textAll.textColor = .grayTitleColor
-            
-            contUnread.backgroundColor = .whiteBubbleColor
-            textUnread.textColor = .grayTitleColor
+        if sender.tag == 0 && ChatWALikeVC.filterMain != 0 {
+            ChatWALikeVC.filterMain = 0
+            self.chats = self.tempChats
+            self.tableView.reloadData()
+        } else if sender.tag == 1 && ChatWALikeVC.filterMain != 1 {
+            ChatWALikeVC.filterMain = 1
+            self.chats = self.tempChats.filter({ Int($0.counter) ?? 0 > 0 })
+            self.tableView.reloadData()
+        }  else if sender.tag == 2 && ChatWALikeVC.filterMain != 2 {
+            ChatWALikeVC.filterMain = 2
+            self.chats = self.tempChats.filter({ !$0.groupId.isEmpty })
+            self.tableView.reloadData()
         }
     }
     
@@ -1022,6 +1030,83 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
         }
     }
     
+    public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        tableView.deselectRow(at: indexPath, animated: true)
+        let data: Chat
+        if isFiltering {
+            data = fillteredData[indexPath.row] as! Chat
+        } else {
+            data = chats[indexPath.row]
+        }
+        if data.isParent {
+            expandCollapseChats(tableView: tableView, indexPath: indexPath)
+            return
+        }
+        if data.pin == "-997" {
+            let smartChatVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "chatGptVC") as! ChatGPTBotView
+            smartChatVC.hidesBottomBarWhenPushed = true
+            smartChatVC.fromNotification = false
+            navigationController?.show(smartChatVC, sender: nil)
+        } else if data.messageScope == MessageScope.WHISPER || data.messageScope == MessageScope.CALL || data.messageScope == MessageScope.MISSED_CALL {
+            if data.pin.isEmpty {
+                return
+            }
+            let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
+            editorPersonalVC.hidesBottomBarWhenPushed = true
+            editorPersonalVC.unique_l_pin = data.pin
+            navigationController?.show(editorPersonalVC, sender: nil)
+        } else {
+            if data.pin.isEmpty {
+                return
+            }
+            let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
+            editorGroupVC.hidesBottomBarWhenPushed = true
+            editorGroupVC.unique_l_pin = data.pin
+            navigationController?.show(editorGroupVC, sender: nil)
+        }
+    }
+    
+    func expandCollapseChats(tableView: UITableView, indexPath: IndexPath) {
+        let data: Chat
+        if isFiltering || selectedTag == UNREAD_TAG {
+            data = fillteredData[indexPath.row] as! Chat
+        } else {
+            data = chats[indexPath.row]
+        }
+        data.isSelected = !data.isSelected
+        if data.isSelected {
+            for dataSubChat in self.chatGroupMaps[data.groupId]! {
+                if var indexParent = chats.firstIndex(where: { $0.isParent && $0.groupId == data.groupId }) {
+                    if isFiltering || selectedTag == UNREAD_TAG {
+                        if var indexParentFilter = fillteredData.firstIndex(where: { ($0 as! Chat).isParent && ($0 as! Chat).groupId == data.groupId }) {
+                            fillteredData.insert(dataSubChat, at: indexParentFilter + 1)
+                            indexParentFilter+=1
+                        }
+                    } else {
+                        chats.insert(dataSubChat, at: indexParent + 1)
+                        indexParent+=1
+                        if var indexParentFilter = tempChats.firstIndex(where: { $0.isParent && $0.groupId == data.groupId }) {
+                            tempChats.insert(dataSubChat, at: indexParentFilter + 1)
+                            indexParentFilter+=1
+                        }
+                    }
+                }
+                
+            }
+        } else {
+            if isFiltering || selectedTag == UNREAD_TAG {
+                if var changedFillteredData = fillteredData as? [Chat] {
+                    changedFillteredData.removeAll(where: { $0.isParent == false && $0.groupId == data.groupId })
+                    self.fillteredData = changedFillteredData
+                }
+            } else {
+                chats.removeAll(where: { $0.isParent == false && $0.groupId == data.groupId })
+                tempChats.removeAll(where: { $0.isParent == false && $0.groupId == data.groupId })
+            }
+        }
+        tableView.reloadData()
+    }
+    
     private func getRealStatus(messageId: String) -> String {
         var status = "1"
         Database.shared.database?.inTransaction({ (fmdb, rollback) in

+ 52 - 36
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -158,10 +158,13 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         navigationController?.navigationBar.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
         navigationController?.navigationBar.tintColor = .white
         navigationController?.navigationBar.overrideUserInterfaceStyle = .dark
+        self.setNeedsStatusBarAppearanceUpdate()
         navigationController?.navigationBar.barStyle = .black
         if self.navigationController?.isNavigationBarHidden ?? false {
             self.navigationController?.setNavigationBarHidden(false, animated: false)
         }
+        navigationController?.navigationBar.prefersLargeTitles = false
+        navigationController?.navigationItem.largeTitleDisplayMode = .never
         updateProfile()
         let indexPath = tableChatView.indexPathsForVisibleRows?.first
         if indexPath != nil && currentIndexpath != nil {
@@ -2330,48 +2333,62 @@ extension EditorGroup: ImageVideoPickerDelegate, PreviewAttachmentImageVideoDele
             let cancelButtonAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
             UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes , for: .normal)
         }
-        guard let result = results.first else { return }
-        if result.itemProvider.hasItemConformingToTypeIdentifier("com.compuserve.gif") {
+        guard let result = results.first else {
             picker.dismiss(animated: true, completion: nil)
-            result.itemProvider.loadDataRepresentation(forTypeIdentifier: "com.compuserve.gif") { data, error in
-                if let error = error {
-                    print("Error loading GIF: \(error.localizedDescription)")
-                } else if let data = data {
-                    DispatchQueue.main.async {
-                        let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
-                        if (self.textFieldSend.textColor != .lightGray) {
-                            previewImageVC.currentTextTextField = self.textFieldSend.text
+            return
+        }
+        if result.itemProvider.hasItemConformingToTypeIdentifier("com.compuserve.gif") {
+            picker.dismiss(animated: true, completion: {
+                Nexilis.showLoader()
+                result.itemProvider.loadDataRepresentation(forTypeIdentifier: "com.compuserve.gif") { data, error in
+                    if let error = error {
+                        print("Error loading GIF: \(error.localizedDescription)")
+                        Nexilis.hideLoader() {
+                            
+                        }
+                    } else if let data = data {
+                        DispatchQueue.main.async {
+                            Nexilis.hideLoader() {
+                                let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
+                                if (self.textFieldSend.textColor != .lightGray) {
+                                    previewImageVC.currentTextTextField = self.textFieldSend.text
+                                }
+                                previewImageVC.fromCopy = true
+                                previewImageVC.isGIF = true
+                                previewImageVC.dataGIF = data
+                                previewImageVC.modalPresentationStyle = .custom
+                                previewImageVC.delegate = self
+                                previewImageVC.isAck = self.isAck
+                                previewImageVC.isConfidential = self.isConfidential
+                                self.present(previewImageVC, animated: true, completion: nil)
+                            }
                         }
-                        previewImageVC.fromCopy = true
-                        previewImageVC.isGIF = true
-                        previewImageVC.dataGIF = data
-                        previewImageVC.modalPresentationStyle = .custom
-                        previewImageVC.delegate = self
-                        previewImageVC.isAck = self.isAck
-                        previewImageVC.isConfidential = self.isConfidential
-                        self.present(previewImageVC, animated: true, completion: nil)
                     }
                 }
-            }
+            })
         } else if result.itemProvider.hasItemConformingToTypeIdentifier("public.image") {
-            picker.dismiss(animated: true, completion: nil)
-            result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
-                if let image = object as? UIImage {
-                    DispatchQueue.main.async {
-                        let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
-                        if (self.textFieldSend.textColor != .lightGray) {
-                            previewImageVC.currentTextTextField = self.textFieldSend.text
+            picker.dismiss(animated: true, completion: {
+                Nexilis.showLoader()
+                result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
+                    if let image = object as? UIImage {
+                        DispatchQueue.main.async {
+                            Nexilis.hideLoader {
+                                let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
+                                if (self.textFieldSend.textColor != .lightGray) {
+                                    previewImageVC.currentTextTextField = self.textFieldSend.text
+                                }
+                                previewImageVC.fromCopy = true
+                                previewImageVC.image = image
+                                previewImageVC.modalPresentationStyle = .custom
+                                previewImageVC.delegate = self
+                                previewImageVC.isAck = self.isAck
+                                previewImageVC.isConfidential = self.isConfidential
+                                self.present(previewImageVC, animated: true, completion: nil)
+                            }
                         }
-                        previewImageVC.fromCopy = true
-                        previewImageVC.image = image
-                        previewImageVC.modalPresentationStyle = .custom
-                        previewImageVC.delegate = self
-                        previewImageVC.isAck = self.isAck
-                        previewImageVC.isConfidential = self.isConfidential
-                        self.present(previewImageVC, animated: true, completion: nil)
                     }
                 }
-            }
+            })
         } else if result.itemProvider.hasItemConformingToTypeIdentifier("public.movie") {
             picker.dismiss(animated: true, completion: {
                 Nexilis.showLoader()
@@ -2421,9 +2438,8 @@ extension EditorGroup: UIDocumentPickerDelegate, DocumentPickerDelegate, QLPrevi
             let navController = CustomNavigationController(rootViewController: previewController)
             navController.navigationBar.tintColor = .white
             navController.navigationBar.barTintColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
+            navController.navigationBar.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
             navController.navigationBar.isTranslucent = false
-            navController.navigationBar.overrideUserInterfaceStyle = .dark
-            navController.navigationBar.barStyle = .black
             let cancelButtonAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
             UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes, for: .normal)
             let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]

+ 52 - 38
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -152,10 +152,13 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         navigationController?.navigationBar.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
         navigationController?.navigationBar.tintColor = .white
         navigationController?.navigationBar.overrideUserInterfaceStyle = .dark
+        self.setNeedsStatusBarAppearanceUpdate()
         navigationController?.navigationBar.barStyle = .black
         if self.navigationController?.isNavigationBarHidden ?? false {
             self.navigationController?.setNavigationBarHidden(false, animated: false)
         }
+        navigationController?.navigationBar.prefersLargeTitles = false
+        navigationController?.navigationItem.largeTitleDisplayMode = .never
         updateProfile()
         gettingDataMessage = false
         let indexPath = tableChatView.indexPathsForVisibleRows?.first
@@ -3649,50 +3652,62 @@ extension EditorPersonal: PreviewAttachmentImageVideoDelegate, PHPickerViewContr
             let cancelButtonAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
             UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes , for: .normal)
         }
-        guard let result = results.first else { return }
-        if result.itemProvider.hasItemConformingToTypeIdentifier("com.compuserve.gif") {
+        guard let result = results.first else {
             picker.dismiss(animated: true, completion: nil)
-            result.itemProvider.loadDataRepresentation(forTypeIdentifier: "com.compuserve.gif") { data, error in
-                if let error = error {
-                    print("Error loading GIF: \(error.localizedDescription)")
-                } else if let data = data {
-                    DispatchQueue.main.async {
-                        let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
-                        if (self.textFieldSend.textColor != .lightGray) {
-                            previewImageVC.currentTextTextField = self.textFieldSend.text
+            return
+        }
+        if result.itemProvider.hasItemConformingToTypeIdentifier("com.compuserve.gif") {
+            picker.dismiss(animated: true, completion: {
+                Nexilis.showLoader()
+                result.itemProvider.loadDataRepresentation(forTypeIdentifier: "com.compuserve.gif") { data, error in
+                    if let error = error {
+                        print("Error loading GIF: \(error.localizedDescription)")
+                        Nexilis.hideLoader() {
+                            
+                        }
+                    } else if let data = data {
+                        DispatchQueue.main.async {
+                            Nexilis.hideLoader() {
+                                let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
+                                if (self.textFieldSend.textColor != .lightGray) {
+                                    previewImageVC.currentTextTextField = self.textFieldSend.text
+                                }
+                                previewImageVC.fromCopy = true
+                                previewImageVC.isGIF = true
+                                previewImageVC.dataGIF = data
+                                previewImageVC.modalPresentationStyle = .custom
+                                previewImageVC.delegate = self
+                                previewImageVC.isAck = self.isAck
+                                previewImageVC.isConfidential = self.isConfidential
+                                self.present(previewImageVC, animated: true, completion: nil)
+                            }
                         }
-                        previewImageVC.fromCopy = true
-                        previewImageVC.isGIF = true
-                        previewImageVC.dataGIF = data
-                        previewImageVC.modalPresentationStyle = .custom
-                        previewImageVC.delegate = self
-                        previewImageVC.isAck = self.isAck
-                        previewImageVC.isConfidential = self.isConfidential
-                        previewImageVC.isCC = self.isContactCenter
-                        self.present(previewImageVC, animated: true, completion: nil)
                     }
                 }
-            }
+            })
         } else if result.itemProvider.hasItemConformingToTypeIdentifier("public.image") {
-            picker.dismiss(animated: true, completion: nil)
-            result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
-                if let image = object as? UIImage {
-                    DispatchQueue.main.async {
-                        let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
-                        if (self.textFieldSend.textColor != .lightGray) {
-                            previewImageVC.currentTextTextField = self.textFieldSend.text
+            picker.dismiss(animated: true, completion: {
+                Nexilis.showLoader()
+                result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
+                    if let image = object as? UIImage {
+                        DispatchQueue.main.async {
+                            Nexilis.hideLoader {
+                                let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
+                                if (self.textFieldSend.textColor != .lightGray) {
+                                    previewImageVC.currentTextTextField = self.textFieldSend.text
+                                }
+                                previewImageVC.fromCopy = true
+                                previewImageVC.image = image
+                                previewImageVC.modalPresentationStyle = .custom
+                                previewImageVC.delegate = self
+                                previewImageVC.isAck = self.isAck
+                                previewImageVC.isConfidential = self.isConfidential
+                                self.present(previewImageVC, animated: true, completion: nil)
+                            }
                         }
-                        previewImageVC.fromCopy = true
-                        previewImageVC.image = image
-                        previewImageVC.modalPresentationStyle = .custom
-                        previewImageVC.delegate = self
-                        previewImageVC.isAck = self.isAck
-                        previewImageVC.isConfidential = self.isConfidential
-                        previewImageVC.isCC = self.isContactCenter
-                        self.present(previewImageVC, animated: true, completion: nil)
                     }
                 }
-            }
+            })
         } else if result.itemProvider.hasItemConformingToTypeIdentifier("public.movie") {
             picker.dismiss(animated: true, completion: {
                 Nexilis.showLoader()
@@ -3744,9 +3759,8 @@ extension EditorPersonal: UIDocumentPickerDelegate, DocumentPickerDelegate, QLPr
             let navController = CustomNavigationController(rootViewController: previewController)
             navController.navigationBar.tintColor = .white
             navController.navigationBar.barTintColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
+            navController.navigationBar.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
             navController.navigationBar.isTranslucent = false
-            navController.navigationBar.overrideUserInterfaceStyle = .dark
-            navController.navigationBar.barStyle = .black
             let cancelButtonAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
             UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes, for: .normal)
             let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]

+ 73 - 13
NexilisLite/NexilisLite/Source/View/Control/SettingTableViewController.swift

@@ -170,6 +170,24 @@ public class SettingTableViewController: UITableViewController, UIGestureRecogni
                                             dataImage["name"] = imageSignIn
                                             NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
                                         }
+                                    } else if FileEncryption.shared.isSecureExists(filename: imageSignIn) {
+                                        do {
+                                            if var data = try FileEncryption.shared.readSecure(filename: imageSignIn) {
+                                                let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                                if dataDecrypt != nil {
+                                                    data = dataDecrypt!
+                                                }
+                                                let image = UIImage(data: data)
+                                                Item.menus["Personal"]?[0].icon = image?.circleMasked
+                                                if !imageSignIn.isEmpty {
+                                                    var dataImage: [AnyHashable : Any] = [:]
+                                                    dataImage["name"] = imageSignIn
+                                                    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                                }
+                                            }
+                                        } catch {
+                                            
+                                        }
                                     } else {
                                         Download().start(forKey: image!) { (name, progress) in
                                             guard progress == 100 else {
@@ -177,13 +195,24 @@ public class SettingTableViewController: UITableViewController, UIGestureRecogni
                                             }
 
                                             DispatchQueue.main.async {
-                                                let image = UIImage(contentsOfFile: file.path)
-                                                Item.menus["Personal"]?[0].icon = image?.circleMasked
-                                                self.tableView.reloadData()
-                                                if !imageSignIn.isEmpty {
-                                                    var dataImage: [AnyHashable : Any] = [:]
-                                                    dataImage["name"] = imageSignIn
-                                                    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                                if FileEncryption.shared.isSecureExists(filename: imageSignIn) {
+                                                    do {
+                                                        if var data = try FileEncryption.shared.readSecure(filename: imageSignIn) {
+                                                            let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                                            if dataDecrypt != nil {
+                                                                data = dataDecrypt!
+                                                            }
+                                                            let image = UIImage(data: data)
+                                                            Item.menus["Personal"]?[0].icon = image?.circleMasked
+                                                            if !imageSignIn.isEmpty {
+                                                                var dataImage: [AnyHashable : Any] = [:]
+                                                                dataImage["name"] = imageSignIn
+                                                                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                                            }
+                                                        }
+                                                    } catch {
+                                                        
+                                                    }
                                                 }
                                             }
                                         }
@@ -213,18 +242,49 @@ public class SettingTableViewController: UITableViewController, UIGestureRecogni
                                     var dataImage: [AnyHashable : Any] = [:]
                                     dataImage["name"] = imageSignIn
                                     NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                } else if FileEncryption.shared.isSecureExists(filename: imageSignIn) {
+                                    do {
+                                        if var data = try FileEncryption.shared.readSecure(filename: imageSignIn) {
+                                            let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                            if dataDecrypt != nil {
+                                                data = dataDecrypt!
+                                            }
+                                            let image = UIImage(data: data)
+                                            Item.menus["Personal"]?[0].icon = image?.circleMasked
+                                            if !imageSignIn.isEmpty {
+                                                var dataImage: [AnyHashable : Any] = [:]
+                                                dataImage["name"] = imageSignIn
+                                                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                            }
+                                        }
+                                    } catch {
+                                        
+                                    }
                                 } else {
                                     Download().start(forKey: imageSignIn) { (name, progress) in
                                         guard progress == 100 else {
                                             return
                                         }
                                         DispatchQueue.main.async {
-                                            let image = UIImage(contentsOfFile: file.path)
-                                            Item.menus["Personal"]?[0].icon = image?.circleMasked
-                                            self.tableView.reloadData()
-                                            var dataImage: [AnyHashable : Any] = [:]
-                                            dataImage["name"] = imageSignIn
-                                            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                            if FileEncryption.shared.isSecureExists(filename: imageSignIn) {
+                                                do {
+                                                    if var data = try FileEncryption.shared.readSecure(filename: imageSignIn) {
+                                                        let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                                                        if dataDecrypt != nil {
+                                                            data = dataDecrypt!
+                                                        }
+                                                        let image = UIImage(data: data)
+                                                        Item.menus["Personal"]?[0].icon = image?.circleMasked
+                                                        if !imageSignIn.isEmpty {
+                                                            var dataImage: [AnyHashable : Any] = [:]
+                                                            dataImage["name"] = imageSignIn
+                                                            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                                                        }
+                                                    }
+                                                } catch {
+                                                    
+                                                }
+                                            }
                                         }
                                     }
                                 }

+ 1 - 1
NexilisLite/Podfile

@@ -7,7 +7,7 @@ target 'NexilisLite' do
 
   # Pods for NexilisLite
 
-  pod 'nuSDKService', '4.0.19'
+  pod 'nuSDKService', '4.0.20'
   pod 'FMDB', '~> 2.7.12'
   pod 'NotificationBannerSwift', :git => 'https://github.com/Daltron/NotificationBanner.git', :tag => '4.0.0'
   pod 'Alamofire', '~> 5.10.2'

+ 1 - 1
StreamShield/Podfile

@@ -6,6 +6,6 @@ target 'StreamShield' do
   use_frameworks!
 
   # Pods for StreamShield
-  pod 'nuSDKService', '~> 4.0.19'
+  pod 'nuSDKService', '~> 4.0.20'
 
 end

+ 1 - 1
StreamShield/StreamShield.podspec

@@ -22,7 +22,7 @@ Pod::Spec.new do |spec|
   spec.source_files = 'StreamShield/Source/**/*'
   spec.resource_bundles = { 'StreamShield' => ['StreamShield/Resource/**/*']}
   spec.swift_version = '5.5.1'
-  spec.dependency 'nuSDKService', '~> 4.0.19'
+  spec.dependency 'nuSDKService', '~> 4.0.20'
   spec.ios.vendored_frameworks = "StreamShield.framework"
   spec.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64', 'ENABLE_BITCODE' => 'NO' }
   spec.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64', 'ENABLE_BITCODE' => 'NO' }