Browse Source

update fix bugs and release for 5.0.58

alqindiirsyam 1 tháng trước cách đây
mục cha
commit
e91909203d

+ 4 - 4
AppBuilder/AppBuilder.xcodeproj/project.pbxproj

@@ -568,7 +568,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.56;
+				MARKETING_VERSION = 5.0.58;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -604,7 +604,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.56;
+				MARKETING_VERSION = 5.0.58;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -640,7 +640,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.56;
+				MARKETING_VERSION = 5.0.58;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -679,7 +679,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.56;
+				MARKETING_VERSION = 5.0.58;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 1 - 0
AppBuilder/AppBuilder/SecondTabViewController.swift

@@ -413,6 +413,7 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
     
     @objc func startConversation(){
         APIS.openChat()
+//        Nexilis.debugBroadcast()
     }
     
     @objc func recordAudio(){

+ 21 - 6
AppBuilder/AppBuilderShare/ShareViewController.swift

@@ -201,16 +201,12 @@ class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDat
                         if selectedImage != nil {
                             try? UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 0.25)?.write(to: sharedThumbURL)
                             if let dataImage = UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 1.0) {
-                                if let compressed = compressImageLikeWhatsApp(UIImage(data: dataImage) ?? UIImage()) {
-                                    try? compressed.write(to: sharedImageURL)
-                                }
+                                try? dataImage.write(to: sharedImageURL)
                             }
                         } else {
                             try? selectedImageTypeImage?.jpegData(compressionQuality: 0.25)?.write(to: sharedThumbURL)
                             if let dataImage = selectedImageTypeImage?.jpegData(compressionQuality: 1.0) {
-                                if let compressed = compressImageLikeWhatsApp(UIImage(data: dataImage) ?? UIImage()) {
-                                    try? compressed.write(to: sharedImageURL)
-                                }
+                                try? dataImage.write(to: sharedImageURL)
                             }
                         }
                     }
@@ -641,6 +637,25 @@ class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDat
                                 
                                 buildAppearance(contact, viewVc)
                                 
+                                vcHandleImage.modalPresentationStyle = .fullScreen
+                                self.navigationController?.present(vcHandleImage, animated: true)
+                            }
+                        }
+                    } else if let data = imageItem as? Data {
+                        DispatchQueue.main.async { [self] in
+                            let image = UIImage(data: data)
+                            typeShareNum = TypeShare.image
+                            selectedImageTypeImage = image
+                            if let viewVc = vcHandleImage.view {
+                                let imageView = UIImageView()
+                                imageView.image = image
+                                imageView.contentMode = .scaleAspectFit
+                                imageView.clipsToBounds = true
+                                viewVc.addSubview(imageView)
+                                imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 150)
+                                
+                                buildAppearance(contact, viewVc)
+                                
                                 vcHandleImage.modalPresentationStyle = .fullScreen
                                 self.navigationController?.present(vcHandleImage, animated: true)
                             }

+ 2 - 0
NexilisLite/NexilisLite/Resource/id.lproj/Localizable.strings

@@ -301,6 +301,8 @@
 "Start Whiteboard" = "Mulai Whiteboard";
 "Start Screen Sharing" = "Mulai Screen Sharing";
 "Waiting for answer" = "Menunggu Jawaban";
+"Calling" = "Memanggil";
+"Ringing" = "Berdering";
 "Incoming Whiteboard" = "Masuk Whiteboard";
 "Incoming Screen Sharing" = "Masuk Screen Sharing";
 "End Whiteboard Session" = "Akhiri Sesi Whiteboard";

+ 28 - 10
NexilisLite/NexilisLite/Source/APIS.swift

@@ -1561,7 +1561,6 @@ public class APIS: NSObject {
     public static func showNotificationNexilis(_ userInfo: [AnyHashable : Any], _ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
         DispatchQueue.main.async {
             if checkAppStateisBackground() {
-                print("SHOW NOTIF ON BACKGROUND")
                 DispatchQueue.global().async {
                     if let payload = userInfo["payload"] as? [String: Any] {
                         if let messagePayload = payload["message"] as? [String: Any] {
@@ -1658,7 +1657,7 @@ public class APIS: NSObject {
                             }
                         }
                     } else if let message_id = userInfo["message_id"] as? String {
-                        getMessageById(id: message_id, completionHandler)
+                        getMessageById(id: message_id, completionHandler: completionHandler)
                     }
                 }
             }
@@ -1727,7 +1726,7 @@ public class APIS: NSObject {
         }
     }
     
-    private static func getMessageById(id: String, _ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
+    private static func getMessageById(id: String, retry: Int = 0, completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
         DispatchQueue.global().async {
             let parameter: [String : Any] = [
                 "pin": User.getMyPin() ?? "",
@@ -1735,7 +1734,12 @@ public class APIS: NSObject {
             ]
             Utils.postDataWithCookiesAndUserAgent(from: URL(string: Utils.getDomainOpr() + "pull_notification")!, parameter: parameter, isFormData: true) { data, response, error in
                 if error != nil {
-                    completionHandler(.failed)
+                    let ret = retry + 1
+                    if ret <= 5 {
+                        getMessageById(id: id, retry: ret, completionHandler: completionHandler)
+                    } else {
+                        completionHandler(.failed)
+                    }
                 } else if let data = data {
                     do {
                         if let dataString = String(data: data, encoding: .utf8) {
@@ -1757,22 +1761,35 @@ public class APIS: NSObject {
                                         IncomingThread.dispatch = nil
                                     }
                                 }
-                                print("save from APIS")
+//                                print("save from APIS")
                                 Nexilis.saveMessage(message: message, withStatus: false, fromAPNS: true)
                                 ackAPN(id: id)
                                 DispatchQueue.main.async {
                                     UIApplication.shared.applicationIconBadgeNumber = Int(APIS.getTotalCounter())
                                 }
-                                completionHandler(.newData)
+                            } else {
+                                let ret = retry + 1
+                                if ret <= 5 {
+                                    getMessageById(id: id, retry: ret, completionHandler: completionHandler)
+                                } else {
+                                    completionHandler(.failed)
+                                }
                             }
                         }
                     } catch {
-                        
+                        let ret = retry + 1
+                        if ret <= 5 {
+                            getMessageById(id: id, retry: ret, completionHandler: completionHandler)
+                        } else {
+                            completionHandler(.failed)
+                        }
                     }
                 } else {
-                    completionHandler(.failed)
-                    DispatchQueue.main.async {
-                        UIApplication.shared.applicationIconBadgeNumber = Int(APIS.getTotalCounter())
+                    let ret = retry + 1
+                    if ret <= 5 {
+                        getMessageById(id: id, retry: ret, completionHandler: completionHandler)
+                    } else {
+                        completionHandler(.failed)
                     }
                 }
             }
@@ -2225,6 +2242,7 @@ public class APIS: NSObject {
                         }
                     }
                 }
+                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "checkNewMessagesNexilis"), object: nil, userInfo: nil)
             }
         }
         afterEnterBackground = true

+ 70 - 52
NexilisLite/NexilisLite/Source/Extension.swift

@@ -455,83 +455,101 @@ extension NSObject {
     
     private static var urlStore = [String:String]()
 
-    public func getImage(name url: String, placeholderImage: UIImage? = nil, isCircle: Bool = false, tableView: UITableView? = nil, indexPath: IndexPath? = nil, isResized: Bool = true, completion: @escaping (Bool, Bool, UIImage?)->()) {
+    public func getImage(
+        name url: String,
+        placeholderImage: UIImage? = nil,
+        isCircle: Bool = false,
+        tableView: UITableView? = nil,
+        indexPath: IndexPath? = nil,
+        isResized: Bool = true,
+        completion: @escaping (Bool, Bool, UIImage?) -> ()
+    ) {
         let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
         type(of: self).urlStore[tmpAddress] = url
-        if url.isEmpty {
+        
+        // Handle empty URL
+        guard !url.isEmpty else {
             completion(false, false, placeholderImage)
             return
         }
+        
         do {
-            let documentDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
+            let documentDir = try FileManager.default.url(for: .documentDirectory,
+                                                          in: .userDomainMask,
+                                                          appropriateFor: nil,
+                                                          create: true)
             let file = documentDir.appendingPathComponent(url)
             if FileManager().fileExists(atPath: file.path) {
-                var image = UIImage(contentsOfFile: file.path)?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
+                var image = UIImage(contentsOfFile: file.path)
                 if isResized {
-                    completion(true, false, isCircle ? image?.circleMasked : image)
-                } else {
-                    completion(true, false, isCircle ? UIImage(contentsOfFile: file.path)?.circleMasked : UIImage(contentsOfFile: file.path))
+                    image = image?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
                 }
-            } else if var tempData = try FileEncryption.shared.readSecure(filename: url) {
-                let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: tempData)
-                if dataDecrypt != nil {
-                    tempData = dataDecrypt!
+                if isCircle {
+                    image = image?.circleMasked
+                }
+                completion(true, false, image)
+                return
+            }
+            if var tempData = try? FileEncryption.shared.readSecure(filename: url) {
+                if let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: tempData) {
+                    tempData = dataDecrypt
                 }
-                let image = UIImage(data: tempData)?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
-//                FileEncryption.shared.wipeData(&tempData)
+                var image = UIImage(data: tempData)
                 if isResized {
-                    completion(true, false, isCircle ? image?.circleMasked : image)
-                } else {
-                    completion(true, false, isCircle ? UIImage(data: tempData)?.circleMasked : UIImage(data: tempData))
+                    image = image?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
                 }
-            } else {
-//                completion(false, false, placeholderImage)
-                Download().startHTTP(forKey: url) { (name, progress) in
-                    guard progress == 100 else {
-                        return
-                    }
-                    
-                    DispatchQueue.main.async {
-                        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)
+                if isCircle {
+                    image = image?.circleMasked
+                }
+                completion(true, false, image)
+                return
+            }
+            
+            // ❌ 3. Not available locally → fallback to download (async)
+            completion(false, false, placeholderImage) // give placeholder early
+            
+            Download().startHTTP(forKey: url) { (name, progress) in
+                guard progress == 100 else { return }
+                
+                DispatchQueue.main.async {
+                    if type(of: self).urlStore[tmpAddress] == name && tableView == nil {
+                        if FileManager().fileExists(atPath: file.path) {
+                            var image = UIImage(contentsOfFile: file.path)
+                            if isResized {
+                                image = image?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
+                            }
+                            if isCircle {
+                                image = image?.circleMasked
+                            }
+                            completion(true, true, image)
+                        } else if FileEncryption.shared.isSecureExists(filename: url) {
+                            if var imageData = try? FileEncryption.shared.readSecure(filename: url) {
+                                if let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: imageData) {
+                                    imageData = dataDecrypt
+                                }
+                                var image = UIImage(data: imageData)
                                 if isResized {
-                                    completion(true, false, isCircle ? image?.circleMasked : image)
-                                } else {
-                                    completion(true, false, isCircle ? UIImage(contentsOfFile: file.path)?.circleMasked : UIImage(contentsOfFile: file.path))
+                                    image = image?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
                                 }
-                            } else if FileEncryption.shared.isSecureExists(filename: url) {
-                                do {
-                                    if var imageData = try FileEncryption.shared.readSecure(filename: url) {
-                                        let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: imageData)
-                                        if dataDecrypt != nil {
-                                            imageData = dataDecrypt!
-                                        }
-                                        let image = UIImage(data: imageData)?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
-                                        if isResized {
-                                            completion(true, false, isCircle ? image?.circleMasked : image)
-                                        } else {
-                                            completion(true, false, isCircle ? UIImage(data: imageData)?.circleMasked : UIImage(data: imageData))
-                                        }
-                                    }
-                                } catch {
-                                    
+                                if isCircle {
+                                    image = image?.circleMasked
                                 }
+                                completion(true, true, image)
                             }
                         }
+                    } else if let tableView = tableView {
+                        tableView.reloadData()
                     }
                 }
             }
+            
         } catch {
+            // In case documentDir fetch fails
             completion(false, false, placeholderImage)
             Download().startHTTP(forKey: url) { (name, progress) in
-                guard progress == 100 else {
-                    return
-                }
-                
+                guard progress == 100 else { return }
                 DispatchQueue.main.async {
-                    guard let tableView = tableView else { return }
-                    tableView.reloadData()
+                    tableView?.reloadData()
                 }
             }
         }

+ 5 - 6
NexilisLite/NexilisLite/Source/IncomingThread.swift

@@ -1392,12 +1392,11 @@ 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 {
+            if APIS.checkAppStateisBackground() {
+                UIApplication.shared.applicationIconBadgeNumber = Int(APIS.getTotalCounter())
+            }
+        }
         //print("save message incoming")
         ack(message: message)
     }

+ 31 - 10
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -19,7 +19,7 @@ import CryptoKit
 import WebKit
 
 public class Nexilis: NSObject {
-    public static var cpaasVersion = "5.0.56"
+    public static var cpaasVersion = "5.0.58"
     public static var sAPIKey = ""
     
     public static var ADDRESS = ""
@@ -159,7 +159,7 @@ public class Nexilis: NSObject {
             try MasterKeyUtil.shared.generateAndStoreMasterKey()
             try MasterKeyUtil.shared.generateAndStorePrefsKey()
             if Utils.getCertificatePinningWebview().isEmpty {
-                let cert: [String: String] = ["nexilis.io": Utils.decrypt(str: "7^reFspLRnRz3NaVjeI2AUQ0l5JFQbf0bZZ3dfYaBMqnL"), "newuniverse.io": Utils.decrypt(str: "6]umyRKg9l6D2N?2wVaejmtPrWNtVKpjqt0mqyA68XwFi")]
+                let cert: [String: String] = ["nexilis.io": Utils.decrypt(str: "6]W6LyyA5NGynzKr3SQbHULW4ghD57B2qvuCOliRoaFEt"), "newuniverse.io": Utils.decrypt(str: "2?iVFEW74Edu3HKUcELqAaQ6cAGmVnYrgdTmW6WoL5N8V")]
                 if let jsonData = try? JSONSerialization.data(withJSONObject: cert, options: []),
                    let jsonString = String(data: jsonData, encoding: .utf8) {
                     Utils.setCertificatePinningWebview(value: jsonString)
@@ -931,11 +931,11 @@ public class Nexilis: NSObject {
     
     public static func addQueueMessage(message: TMessage, isEditMessage: Bool = false) {
 //        OutgoingThread.default.addQueue(message: message)
-        if isEditMessage {
+//        if isEditMessage {
             OutgoingThread.default.addQueue(message: message)
-        } else {
-            InquiryThread.default.addQueue(message: message)
-        }
+//        } else {
+//            InquiryThread.default.addQueue(message: message)
+//        }
     }
     
     public static func deleteQueueMessage(message: TMessage) {
@@ -1393,9 +1393,6 @@ public class Nexilis: NSObject {
             }
             return
         }
-        if id == nil {
-            return
-        }
         if let url = URL(string: "itms-apps://apple.com/app/\(id)") {
             UIApplication.shared.open(url)
         }
@@ -2259,6 +2256,19 @@ public class Nexilis: NSObject {
                                 }
                             }
                             cursorStatus.close()
+                        } else {
+                            if let cursorStatus = Database.shared.getRecords(fmdb: fmdb, query: "SELECT status FROM MESSAGE_STATUS where message_id = '\(t)'"), cursorStatus.next() {
+                                if Int(status)! == 2 {
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
+                                        "status" : status,
+                                        "time_ack" : String(Date().currentTimeMillis()),
+                                        "longitude" : longitude,
+                                        "latitude" : latitude,
+                                        "location" : desc,
+                                        "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(t)'")
+                                }
+                                cursorStatus.close()
+                            }
                         }
                     }
                 } else {
@@ -2299,6 +2309,17 @@ public class Nexilis: NSObject {
                             }
                         }
                         cursorStatus.close()
+                    } else if let cursorStatus = Database.shared.getRecords(fmdb: fmdb, query: "SELECT status FROM MESSAGE_STATUS where message_id = '\(message_id)'"), cursorStatus.next() {
+                        if Int(status)! == 2 {
+                            _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
+                                "status" : status,
+                                "time_ack" : String(Date().currentTimeMillis()),
+                                "longitude" : longitude,
+                                "latitude" : latitude,
+                                "location" : desc,
+                                "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(message_id)'")
+                        }
+                        cursorStatus.close()
                     }
                 }
             } catch {
@@ -3136,7 +3157,7 @@ extension Nexilis: MessageDelegate {
                                                         UIApplication.shared.visibleViewController?.present(dialog, animated: true)
                                                     }
                                                 } else {
-                                                    print("show broadcast no button")
+                                                    print("show broadcast no button \(response.toLogString())")
                                                 }
                                             } catch {
                                                 rollback.pointee = true

+ 39 - 14
NexilisLite/NexilisLite/Source/OutgoingThread.swift

@@ -33,15 +33,30 @@ class OutgoingThread {
                     if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select message, id from OUTGOING") {
                         while cursor.next() {
                             if let message = cursor.string(forColumnIndex: 0) {
-                                if let cursorMessage = Database.shared.getRecords(fmdb: fmdb, query: "select message_id from MESSAGE where message_id = '\(cursor.string(forColumnIndex: 1)!)'") {
-                                    if cursorMessage.next() {
-                                        addQueue(message: TMessage(data: message))
+                                let id = cursor.string(forColumnIndex: 1) ?? ""
+//                                print("KOK ADA: \(id)")
+                                let tMessage = TMessage(data: message)
+                                let message_id = tMessage.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID)
+                                if let cursorM = Database.shared.getRecords(fmdb: fmdb, query: "SELECT status FROM MESSAGE WHERE message_id='\(message_id)'"), cursorM.next() {
+                                    let statusM = cursorM.string(forColumnIndex: 0) ?? "0"
+                                    if let cursorStatus = Database.shared.getRecords(fmdb: fmdb, query: "SELECT status FROM MESSAGE_STATUS WHERE message_id='\(message_id)'") {
+                                        var listStatus: [Int] = []
+                                        while cursorStatus.next() {
+                                            listStatus.append(Int(cursorStatus.string(forColumnIndex: 0)!)!)
+                                        }
+                                        let status = "\(listStatus.min() ?? Int(statusM)!)"
+                                        if status == "1" {
+                                            self.appendAndRunQueue(message: tMessage)
+                                        } else {
+                                            self.delOutgoing(fmdb: fmdb, messageId: id)
+                                        }
+                                        cursorStatus.close()
                                     } else {
-                                        delOutgoing(fmdb: fmdb, messageId: cursor.string(forColumnIndex: 1)!)
+                                        self.delOutgoing(fmdb: fmdb, messageId: id)
                                     }
-                                    cursorMessage.close()
+                                    cursorM.close()
                                 } else {
-                                    delOutgoing(fmdb: fmdb, messageId: cursor.string(forColumnIndex: 1)!)
+                                    self.delOutgoing(fmdb: fmdb, messageId: id)
                                 }
                             }
                         }
@@ -56,6 +71,13 @@ class OutgoingThread {
     }
     
     func addQueue(message: TMessage) {
+        if message.getCode() == CoreMessage_TMessageCode.SEND_CHAT || message.getCode() == CoreMessage_TMessageCode.EDIT_MESSAGE {
+            Nexilis.saveMessage(message: message)
+        }
+        appendAndRunQueue(message: message)
+    }
+    
+    func appendAndRunQueue(message: TMessage) {
         queue.append(message)
         semaphore.signal()
         addOugoing(message: message)
@@ -69,7 +91,8 @@ class OutgoingThread {
     
     private func addOugoing(message: TMessage) {
         DispatchQueue.global().async {
-            let messageId = message.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID)
+            let messageId = message.getStatus()
+//            print("SAVE OUTGOING \(messageId)")
             if !messageId.isEmpty {
                 Database.shared.database?.inTransaction({ (fmdb, rollback) in
                     do {
@@ -87,6 +110,7 @@ class OutgoingThread {
     }
     
     private func delOutgoing(fmdb: Any, messageId: String) {
+//        print("DELETE OUTGOING \(messageId)")
         _ = Database.shared.deleteRecord(fmdb: fmdb as! FMDatabase, table: "OUTGOING", _where: "id = '\(messageId)'")
     }
     
@@ -202,7 +226,7 @@ class OutgoingThread {
                                                 _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
                                                     "status" : response.getBody(key: CoreMessage_TMessageKey.STATUS, default_value: "2")
                                                 ], _where: "message_id = '\(messageId)'")
-                                                self.delOutgoing(fmdb: fmdb, messageId: messageId)
+                                                self.delOutgoing(fmdb: fmdb, messageId: message.getStatus())
                                             } catch {
                                                 rollback.pointee = true
                                                 print("Access database error: \(error.localizedDescription)")
@@ -245,7 +269,7 @@ class OutgoingThread {
                                         _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
                                             "status" : response.getBody(key: CoreMessage_TMessageKey.STATUS, default_value: "2")
                                         ], _where: "message_id = '\(messageId)'")
-                                        self.delOutgoing(fmdb: fmdb, messageId: messageId)
+                                        self.delOutgoing(fmdb: fmdb, messageId: message.getStatus())
                                     } catch {
                                         rollback.pointee = true
                                         print("Access database error: \(error.localizedDescription)")
@@ -277,14 +301,15 @@ class OutgoingThread {
                         _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
                             "status" : response.getBody(key: CoreMessage_TMessageKey.STATUS, default_value: "2")
                         ], _where: "message_id = '\(messageId)'")
-                        self.delOutgoing(fmdb: fmdb, messageId: messageId)
+                        self.delOutgoing(fmdb: fmdb, messageId: message.getStatus())
                     } catch {
                         rollback.pointee = true
                         print("Access database error: \(error.localizedDescription)")
                     }
                 })
             } else {
-                InquiryThread.default.addQueue(message: message)
+//                InquiryThread.default.addQueue(message: message)
+                OutgoingThread.default.addQueue(message: message)
             }
         }
     }
@@ -317,7 +342,7 @@ class OutgoingThread {
                     _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                         "status" : "0"
                     ], _where: "message_id = '\(message.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID))'")
-                    self.delOutgoing(fmdb: fmdb, messageId: message.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID))
+                    self.delOutgoing(fmdb: fmdb, messageId: message.getStatus())
                 } catch {
                     rollback.pointee = true
                     print("Access database error: \(error.localizedDescription)")
@@ -345,7 +370,7 @@ class OutgoingThread {
                                 _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                                     "status" : "0"
                                 ], _where: "message_id = '\(message.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID))'")
-                                self.delOutgoing(fmdb: fmdb, messageId: message.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID))
+                                self.delOutgoing(fmdb: fmdb, messageId: message.getStatus())
                             } catch {
                                 rollback.pointee = true
                                 print("Access database error: \(error.localizedDescription)")
@@ -448,7 +473,7 @@ class OutgoingThread {
                         rollback.pointee = true
                         //print(error)
                     }
-                    self.delOutgoing(fmdb: fmdb, messageId: messageId)
+                    self.delOutgoing(fmdb: fmdb, messageId: message.getStatus())
                 }
             } catch {
                 rollback.pointee = true

+ 175 - 33
NexilisLite/NexilisLite/Source/Utils.swift

@@ -3007,10 +3007,19 @@ public class DialogBroadcastInApp: UIViewController {
     public var listTitleButton: [String] = []
     public var message: [String: Any] = [:]
     
+    private var iconTitleImage: UIImage?
+    private var iconSuffixImage: UIImage?
+    private var buttonBackgroundImages: [Int: UIImage] = [:]
+    
     public override func viewDidLoad() {
         super.viewDidLoad()
-        self.view.backgroundColor = .black.withAlphaComponent(0.5)
-        
+        self.view.backgroundColor = .black.withAlphaComponent(0.3)
+        DispatchQueue.global().async {
+            self.loadDataAndBuildUI()
+        }
+    }
+    
+    private func setupUI() {
         let container = UIView()
         self.view.addSubview(container)
         container.anchor(left: self.view.leftAnchor, right: self.view.rightAnchor, paddingLeft: 20, paddingRight: 20, centerY: self.view.centerYAnchor)
@@ -3027,18 +3036,8 @@ public class DialogBroadcastInApp: UIViewController {
         container.addSubview(title)
         title.anchor(top: container.topAnchor, paddingTop: 15, centerX: container.centerXAnchor, maxWidth: UIScreen.main.bounds.width / 2)
         
-        let imageWarning = UIImageView(image: UIImage(named: "pb_security_warning_green", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!)
-        if !form.iconTitle.isEmpty {
-            getImage(name: form.iconTitle, completion: { result, isDownloaded, image in
-                DispatchQueue.main.async {
-                    if let img = image {
-                        DispatchQueue.main.async {
-                            imageWarning.image = image
-                        }
-                    }
-                }
-            })
-        }
+        let defaultWarningImage = UIImage(named: "pb_security_warning_green", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+        let imageWarning = UIImageView(image: self.iconTitleImage ?? defaultWarningImage)
         container.addSubview(imageWarning)
         imageWarning.anchor(top: container.topAnchor, right: title.leftAnchor, paddingTop: 10, paddingRight: -5, width: 30, height: 30)
         
@@ -3046,16 +3045,8 @@ public class DialogBroadcastInApp: UIViewController {
         container.addSubview(imageLogo)
         imageLogo.anchor(top: container.topAnchor, left: container.leftAnchor, paddingTop: 10, paddingLeft: 10, width: 40, height: 40)
         
-        let imageChat = UIImageView(image: UIImage(named: "pb_startup_iconsuffix", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!)
-        if !form.iconSuffix.isEmpty {
-            getImage(name: form.iconSuffix, completion: { result, isDownloaded, image in
-                if let img = image {
-                    DispatchQueue.main.async {
-                        imageChat.image = image
-                    }
-                }
-            })
-        }
+        let defaultChatImage = UIImage(named: "pb_startup_iconsuffix", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+        let imageChat = UIImageView(image: self.iconSuffixImage ?? defaultChatImage)
         container.addSubview(imageChat)
         imageChat.anchor(top: container.topAnchor, right: container.rightAnchor, paddingTop: 10, paddingRight: 10, width: 30, height: 30)
         
@@ -3128,7 +3119,29 @@ public class DialogBroadcastInApp: UIViewController {
                             "ex_book" : self.message[CoreMessage_TMessageKey.MESSAGE_TEXT] ?? ""
                         ], _where: "message_id = '\(self.message[CoreMessage_TMessageKey.MESSAGE_ID] ?? "")'")
                     })
+                    let messageText = self.message[CoreMessage_TMessageKey.MESSAGE_TEXT] as? String ?? ""
                     var messageTextSend = ""
+                    if var json = try! JSONSerialization.jsonObject(with: messageText.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [String: Any] {
+                        Database.shared.database?.inTransaction({ fmdb, rollback in
+                            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select * from FORM_ITEM where form_id = '\(self.formItem.formId)'"), cursor.next() {
+                                for columnIndex in 0..<cursor.columnCount {
+                                    if let columnName = cursor.columnName(for: columnIndex) {
+                                        if let value = cursor.object(forColumn: columnName) {
+                                            if columnName == "key" {
+                                                json[value as? String ?? ""] = title
+                                                break
+                                            }
+                                        }
+                                    }
+                                }
+                                cursor.close()
+                            }
+                            if let jsonData = try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted),
+                               let jsonString = String(data: jsonData, encoding: .utf8) {
+                                messageTextSend = jsonString
+                            }
+                        })
+                    }
                     let message = CoreMessage_TMessageBank.sendMessage(l_pin: self.form.formId, message_scope_id: MessageScope.FORM, status: "1", message_text: messageTextSend, credential: "0", attachment_flag: "", ex_blog_id: "", message_large_text: "", ex_format: "", image_id: "", audio_id: "", video_id: "", file_id: self.form.formId, thumb_id: "", reff_id: "", read_receipts: "4", chat_id: "", is_call_center: "0", call_center_id: "", opposite_pin: "", specFile: "")
                     OutgoingThread.default.addQueue(message: message)
                     self.dismiss(animated: true)
@@ -3141,15 +3154,10 @@ public class DialogBroadcastInApp: UIViewController {
             } else {
                 let backgrounds = formItem.background.components(separatedBy: ",")
                 if index < backgrounds.count {
-                    let background = backgrounds[index]
                     button.setTitle("", for: .normal)
-                    getImage(name: background, isResized: false, completion: { result, isDownloaded, image in
-                        if let img = image {
-                            DispatchQueue.main.async {
-                                button.setBackgroundImage(img.resizableImage(withCapInsets: .zero, resizingMode: .stretch), for: .normal)
-                            }
-                        }
-                    })
+                    if let img =  buttonBackgroundImages[index] {
+                        button.setBackgroundImage(img.resizableImage(withCapInsets: .zero, resizingMode: .stretch), for: .normal)
+                    }
                 }
             }
             
@@ -3166,7 +3174,43 @@ public class DialogBroadcastInApp: UIViewController {
         footer.numberOfLines = 0
         container.addSubview(footer)
         footer.anchor(top: containerButton.bottomAnchor, bottom: container.bottomAnchor, right: container.rightAnchor, paddingTop: 10, paddingBottom: 5, paddingRight: 10)
-        
+    }
+    
+    private func loadDataAndBuildUI() {
+        let semaphore = DispatchSemaphore(value: 0)
+        if !form.iconTitle.isEmpty {
+            getImage(name: form.iconTitle) { result, _, image in
+                if result, let img = image {
+                    self.iconTitleImage = img
+                    semaphore.signal()
+                }
+            }
+            semaphore.wait()
+        }
+        if !form.iconSuffix.isEmpty {
+            getImage(name: form.iconSuffix) { result, _, image in
+                if result, let img = image {
+                    self.iconSuffixImage = img
+                    semaphore.signal()
+                }
+            }
+            semaphore.wait()
+        }
+        if !formItem.background.isEmpty {
+            let backgrounds = formItem.background.components(separatedBy: ",")
+            for (index, backgroundName) in backgrounds.enumerated() {
+                getImage(name: backgroundName, isResized: false) { result, _, image in
+                    if result, let img = image {
+                        self.buttonBackgroundImages[index] = img
+                        semaphore.signal()
+                    }
+                }
+                semaphore.wait()
+            }
+        }
+        DispatchQueue.main.async {
+            self.setupUI()
+        }
     }
 }
 
@@ -3735,3 +3779,101 @@ class HtmlUtils {
         return false
     }
 }
+
+class FormView: UIView {
+    
+    private var scrollView: UIScrollView!
+    private var stackView: UIStackView!
+    
+    private var resetButton: UIButton!
+    private var submitButton: UIButton!
+    private var rejectButton: UIButton!
+    private var approveButton: UIButton!
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupView()
+    }
+    
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        setupView()
+    }
+    
+    private func setupView() {
+        backgroundColor = .white
+        
+        // ScrollView + Stack
+        scrollView = UIScrollView()
+        scrollView.translatesAutoresizingMaskIntoConstraints = false
+        addSubview(scrollView)
+        
+        stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 10
+        stackView.translatesAutoresizingMaskIntoConstraints = false
+        scrollView.addSubview(stackView)
+        
+        // Buttons
+        resetButton = createButton(title: "Reset".localized(), color: .gray, action: #selector(resetTapped))
+        submitButton = createButton(title: "Submit".localized(), color: .systemBlue, action: #selector(submitTapped))
+        rejectButton = createButton(title: "Reject".localized(), color: .gray, action: #selector(rejectTapped))
+        approveButton = createButton(title: "Approve".localized(), color: .systemBlue, action: #selector(approveTapped))
+        
+        let buttonStack = UIStackView(arrangedSubviews: [resetButton, rejectButton, submitButton, approveButton])
+        buttonStack.axis = .horizontal
+        buttonStack.spacing = 8
+        buttonStack.distribution = .fillEqually
+        buttonStack.translatesAutoresizingMaskIntoConstraints = false
+        addSubview(buttonStack)
+        
+        // Layout
+        NSLayoutConstraint.activate([
+            scrollView.topAnchor.constraint(equalTo: topAnchor),
+            scrollView.leftAnchor.constraint(equalTo: leftAnchor),
+            scrollView.rightAnchor.constraint(equalTo: rightAnchor),
+            scrollView.bottomAnchor.constraint(equalTo: buttonStack.topAnchor, constant: -10),
+            
+            stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
+            stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor),
+            stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor),
+            stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
+            stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
+            
+            buttonStack.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -8),
+            buttonStack.leftAnchor.constraint(equalTo: leftAnchor, constant: 8),
+            buttonStack.rightAnchor.constraint(equalTo: rightAnchor, constant: -8),
+            buttonStack.heightAnchor.constraint(equalToConstant: 50)
+        ])
+    }
+    
+    private func createButton(title: String, color: UIColor, action: Selector) -> UIButton {
+        let button = UIButton(type: .system)
+        button.setTitle(title, for: .normal)
+        button.backgroundColor = color
+        button.setTitleColor(.white, for: .normal)
+        button.layer.cornerRadius = 8
+        button.addTarget(self, action: action, for: .touchUpInside)
+        return button
+    }
+    
+    // MARK: - Actions
+    @objc private func resetTapped() { print("Reset tapped") }
+    @objc private func submitTapped() { print("Submit tapped") }
+    @objc private func rejectTapped() { print("Reject tapped") }
+    @objc private func approveTapped() { print("Approve tapped") }
+    
+    // MARK: - Add Dynamic Fields
+    func addField(_ view: UIView) {
+        stackView.addArrangedSubview(view)
+    }
+}
+
+// MARK: - UILabel convenience init
+extension UILabel {
+    convenience init(text: String) {
+        self.init()
+        self.text = text
+        self.font = UIFont.systemFont(ofSize: 14)
+    }
+}

+ 3 - 1
NexilisLite/NexilisLite/Source/View/BNIView/BNIBookingWebView.swift

@@ -177,8 +177,10 @@ public class BNIBookingWebView: UIViewController, WKNavigationDelegate, UIScroll
         if lang == "id" {
             intLang = 1
         }
-        if stringQMS.starts(with: Utils.getURLBase()) && !stringQMS.contains("/TrustedChannel") && !stringQMS.contains("/get_oneapp") {
+        if stringQMS.contains("?") {
             stringQMS = stringQMS + "&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+        } else {
+            stringQMS = stringQMS + "?lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
         }
         if let url = URL(string: "\(stringQMS)") {
             if !isSecureBrowser {

+ 5 - 5
NexilisLite/NexilisLite/Source/View/Call/QmeraAudioViewController.swift

@@ -105,7 +105,7 @@ class QmeraAudioViewController: UIViewController {
     
     let status: UILabel = {
         let label = UILabel()
-        label.text = "Calling..."
+        label.text = "Calling".localized() + "..."
         label.font = UIFont.systemFont(ofSize: 14)
         label.textColor = .white
         label.textAlignment = .center
@@ -400,7 +400,7 @@ class QmeraAudioViewController: UIViewController {
                         if let response = Nexilis.writeAndWait(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()
+                                    self.status.text = "Ringing".localized() + "..."
                                 }
                             } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "01" {
                                 API.initiateCCall(sParty: u.pin)
@@ -571,7 +571,7 @@ class QmeraAudioViewController: UIViewController {
         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..."
+        status.text = "Calling".localized() + "..."
         view.addSubview(end)
         end.anchor(bottom: view.bottomAnchor, paddingBottom: 60, centerX: view.centerXAnchor, width: buttonSize, height: buttonSize)
         
@@ -584,7 +584,7 @@ class QmeraAudioViewController: UIViewController {
             backToDefaultAudioSession()
             Nexilis.playRingbacktoneCall()
         }
-        status.text = "Connecting..."
+        status.text = "Calling".localized() + "..."
         view.addSubview(end)
         end.anchor(bottom: view.bottomAnchor, paddingBottom: 60, centerX: view.centerXAnchor, width: buttonSize, height: buttonSize)
         
@@ -1144,7 +1144,7 @@ class QmeraAudioViewController: UIViewController {
             } else if state == Nexilis.AUDIO_CALL_RINGING || (!ticketId.isEmpty && state == Nexilis.VIDEO_CALL_RINGING) {
                 if users.count == 1 && !autoAcceptAPN {
                     DispatchQueue.main.async {
-                        self.status.text = "Waiting for answer".localized()
+                        self.status.text = "Ringing".localized() + "..."
                     }
                 }
             } else if state == Nexilis.AUDIO_CALL_OFFHOOK || (!ticketId.isEmpty && state == Nexilis.VIDEO_CALL_OFFHOOK) {

+ 4 - 4
NexilisLite/NexilisLite/Source/View/Call/QmeraVideoViewController.swift

@@ -481,7 +481,7 @@ class QmeraVideoViewController: UIViewController {
             labelIncomingOutgoing.centerXAnchor.constraint(equalTo: view.centerXAnchor)
         ])
         if isInisiator {
-            labelIncomingOutgoing.text = "Connecting".localized()
+            labelIncomingOutgoing.text = "Calling".localized() + "..."
             if ticketId.isEmpty {
                 backToDefaultAudioSession()
                 Nexilis.playRingbacktoneCall()
@@ -490,7 +490,7 @@ class QmeraVideoViewController: UIViewController {
                         if let response = Nexilis.writeAndWait(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()
+                                    self.labelIncomingOutgoing.text = "Ringing".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)
@@ -587,7 +587,7 @@ class QmeraVideoViewController: UIViewController {
                 if let f_pin = data["f_pin"] as? String {
                     if f_pin == User.getMyPin()!  {
                         if !Nexilis.callAPNActivated {
-                            labelIncomingOutgoing.text = "Waiting for answer".localized()
+                            labelIncomingOutgoing.text = "Ringing".localized()
                         }
                         API.initiateCCall(sParty: l_pin, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: listRemoteViewFix, ivLocalView: cameraView, ivRemoteZ: zoomView)
                     }
@@ -1886,7 +1886,7 @@ class QmeraVideoViewController: UIViewController {
                 } else {
                     if self.dataPerson.count == 1 && !self.autoAcceptAPN {
                         DispatchQueue.main.async {
-                            self.labelIncomingOutgoing.text = "Waiting for answer".localized()
+                            self.labelIncomingOutgoing.text = "Ringing".localized() + "..."
                         }
                     }
                 }

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

@@ -104,7 +104,7 @@ class ScreenSharingViewController: UIViewController {
             labelIncomingOutgoing.centerXAnchor.constraint(equalTo: view.centerXAnchor)
         ])
         if fromContact == 0 {
-            labelIncomingOutgoing.text = "Waiting for answer".localized() + "..."
+            labelIncomingOutgoing.text = "Ringing".localized() + "..."
             _ = Nexilis.write(message: CoreMessage_TMessageBank.ssCreate(l_pin: user!.pin))
         } else {
             labelIncomingOutgoing.text = "Incoming Screen Sharing".localized() + "..."

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

@@ -132,7 +132,7 @@ class WhiteboardViewController: UIViewController, WhiteboardDelegate {
             labelIncomingOutgoing.centerXAnchor.constraint(equalTo: view.centerXAnchor)
         ])
         if fromContact == 0 {
-            labelIncomingOutgoing.text = "Waiting for answer".localized() + "..."
+            labelIncomingOutgoing.text = "Ringing".localized() + "..."
             _ = Nexilis.write(message: CoreMessage_TMessageBank.wbCreate(l_pin: user!.pin))
         } else {
             labelIncomingOutgoing.text = "Incoming Whiteboard".localized() + "..."

+ 152 - 28
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -296,6 +296,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         center.addObserver(self, selector: #selector(onMemberTopic(notification:)), name: NSNotification.Name(rawValue: "onTopic"), object: nil)
         center.addObserver(self, selector: #selector(onFailedSendMessage(notification:)), name: NSNotification.Name(rawValue: Nexilis.failedSendMessage), object: nil)
         center.addObserver(self, selector: #selector(onUpdatedMessage(notification:)), name: NSNotification.Name(rawValue: "onUpdatedMessage"), object: nil)
+        center.addObserver(self, selector: #selector(onCheckNewMessages(notification:)), name: NSNotification.Name(rawValue: "checkNewMessagesNexilis"), object: nil)
         
         locationManager.delegate = self
         locationManager.requestWhenInUseAuthorization()
@@ -677,17 +678,18 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         })
     }
     
-    private func getData() {
+    private func getData(offset: Int64 = 0) {
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
-                var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='' AND l_pin='\(dataGroup["group_id"]  as? String ?? "")' order by server_date asc"
+                var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='' AND l_pin='\(dataGroup["group_id"]  as? String ?? "")' order by server_date asc LIMIT -1 OFFSET \(offset)"
                 if isHistoryCC {
-                    query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared FROM MESSAGE where call_center_id='\(complaintId)' order by server_date asc"
+                    query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared FROM MESSAGE where call_center_id='\(complaintId)' order by server_date asc LIMIT -1 OFFSET \(offset)"
                 } else if (dataTopic["chat_id"]  as? String ?? "" != "") {
-                    query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='\(dataTopic["chat_id"]  as? String ?? "")' order by server_date asc"
+                    query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='\(dataTopic["chat_id"]  as? String ?? "")' order by server_date asc LIMIT -1 OFFSET \(offset)"
                 }
                 if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
                     var tempImages: [ImageGrouping] = []
+                    var idxOff = 0
                     while cursorData.next() {
                         var row: [String: Any?] = [:]
                         row["message_id"] = cursorData.string(forColumnIndex: 0)
@@ -819,7 +821,11 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                         } else if tempImages.count != 0 {
                             tempImages.removeAll()
                         }
+                        if offset > 0 && idxOff == 0 {
+                            self.markerCounter = row["message_id"] as? String
+                        }
                         dataMessages.append(row)
+                        idxOff+=1
                     }
     //                if isHistoryCC {
     //                    dataMessages.remove(at: 0)
@@ -853,7 +859,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     }
     
     private func getRealStatus(messageId: String) -> String {
-        var status = "1"
+        var status = "-1"
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
                 if let cursorStatus = Database.shared.getRecords(fmdb: fmdb, query: "SELECT status, f_pin FROM MESSAGE_STATUS WHERE message_id='\(messageId)'") {
@@ -862,7 +868,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                         listStatus.append(Int(cursorStatus.string(forColumnIndex: 0)!)!)
                     }
                     cursorStatus.close()
-                    status = "\(listStatus.min() ?? 2)"
+                    status = "\(listStatus.min() ?? -1)"
                 }
             } catch {
                 rollback.pointee = true
@@ -1087,6 +1093,48 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         updateProgress(data)
     }
     
+    @objc func  onCheckNewMessages(notification: NSNotification) {
+        var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='' AND l_pin='\(dataGroup["group_id"]  as? String ?? "")' order by server_date asc"
+        if isHistoryCC {
+            query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared FROM MESSAGE where call_center_id='\(complaintId)' order by server_date asc"
+        } else if (dataTopic["chat_id"]  as? String ?? "" != "") {
+            query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='\(dataTopic["chat_id"]  as? String ?? "")' order by server_date asc"
+        }
+        var countMessagesNow: Int64 = 0
+        DispatchQueue.main.async { [self] in
+            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                do {
+                    if let cursorCount = Database.shared.getRecords(fmdb: fmdb, query: query), cursorCount.next() {
+                        countMessagesNow = Int64(cursorCount.int(forColumnIndex: 0))
+                        cursorCount.close()
+                    }
+                }catch{}
+            })
+            if dataMessages.count < countMessagesNow {
+                self.counter = Int(countMessagesNow) - dataMessages.count
+                getData(offset: Int64(self.dataMessages.count))
+                tableChatView.reloadData()
+                if !self.indicatorCounterBSTB.isDescendant(of: self.view) && !self.buttonScrollToBottom.isDescendant(of: self.view) {
+                    let indexMessage = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == self.markerCounter })
+                    if indexMessage != nil {
+                        let section = self.dataDates.firstIndex(of: self.dataMessages[indexMessage!]["chat_date"]  as? String ?? "")
+                        let row = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == self.dataMessages[indexMessage!]["chat_date"]  as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == self.dataMessages[indexMessage!]["message_id"] as? String })
+                        self.tableChatView.scrollToRow(at: IndexPath(row: row!, section: section!), at: .top, animated: true)
+                    }
+                } else if self.buttonScrollToBottom.isDescendant(of: self.view) {
+                    if !self.indicatorCounterBSTB.isDescendant(of: self.view) {
+                        addCounterAtButttonScrollToBottom()
+                    } else {
+                        self.labelCounter.text = "\(counter)"
+                    }
+                } else {
+                    addButtonScrollToBottom()
+                    addCounterAtButttonScrollToBottom()
+                }
+            }
+        }
+    }
+    
     @objc func onUpdatedMessage(notification: NSNotification) {
         DispatchQueue.main.async {
             let data:[AnyHashable : Any] = notification.userInfo!
@@ -1865,7 +1913,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         sendChat(message_text: textFieldSend.text!, viewController: self)
     }
     
-    private func sendChat(message_scope_id:String =  MessageScope.GROUP, status:String =  "2", message_text:String =  "", credential:String = "0", attachment_flag: String = "0", ex_blog_id: String = "", message_large_text: String = "", ex_format: String = "", image_id: String = "", audio_id: String = "", video_id: String = "", file_id: String = "", thumb_id: String = "", reff_id: String = "", read_receipts: String = "", is_call_center: String = "0", call_center_id: String = "", viewController: UIViewController, gif_id: String = "", is_forwarded: Int = 0) {
+    private func sendChat(message_scope_id:String =  MessageScope.GROUP, status:String =  "1", message_text:String =  "", credential:String = "0", attachment_flag: String = "0", ex_blog_id: String = "", message_large_text: String = "", ex_format: String = "", image_id: String = "", audio_id: String = "", video_id: String = "", file_id: String = "", thumb_id: String = "", reff_id: String = "", read_receipts: String = "", is_call_center: String = "0", call_center_id: String = "", viewController: UIViewController, gif_id: String = "", is_forwarded: Int = 0) {
         if viewController is EditorGroup && file_id == "" && dataMessageForward == nil {
             if ((textFieldSend.text!.trimmingCharacters(in: .whitespacesAndNewlines) == "Send message".localized() && textFieldSend.textColor == UIColor.lightGray && attachment_flag != "11") || textFieldSend.text!.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ) {
                 dismissKeyboard()
@@ -2472,10 +2520,45 @@ extension EditorGroup: ImageVideoPickerDelegate, PreviewAttachmentImageVideoDele
             picker.dismiss(animated: true, completion: {
                 Nexilis.showLoader(text: "Preparing...".localized())
                 result.itemProvider.loadDataRepresentation(forTypeIdentifier: "com.compuserve.gif") { data, error in
-                    if let error = error {
-                        print("Error loading GIF: \(error.localizedDescription)")
-                        Nexilis.hideLoader() {
-                            
+                    if error != nil {
+                        self.loadAnimatedMedia(from: result.itemProvider) { data, isGIF in
+                            guard let data = data else {
+                                print("Failed to load media")
+                                return
+                            }
+
+                            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
+                                    }
+                                    if isGIF {
+                                        previewImageVC.fromCopy = true
+                                        previewImageVC.isGIF = true
+                                        previewImageVC.dataGIF = data
+                                        previewImageVC.modalPresentationStyle = .custom
+                                        previewImageVC.delegate = self
+                                        previewImageVC.isAck = self.isAck
+                                        previewImageVC.isConfidential = self.isConfidential
+                                    } else {
+                                        let fileManager = FileManager.default
+                                        let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+                                        let destinationURL = documentsDirectory.appendingPathComponent(UUID().uuidString + ".mov")
+                                        do {
+                                            try data.write(to: destinationURL)
+                                            previewImageVC.modalPresentationStyle = .custom
+                                            previewImageVC.urlVideoPhpPicker = destinationURL
+                                            previewImageVC.delegate = self
+                                            previewImageVC.isAck = self.isAck
+                                            previewImageVC.isConfidential = self.isConfidential
+                                        } catch {
+                                            
+                                        }
+                                    }
+                                    self.present(previewImageVC, animated: true, completion: nil)
+                                }
+                            }
                         }
                     } else if let data = data {
                         DispatchQueue.main.async {
@@ -2566,6 +2649,37 @@ extension EditorGroup: ImageVideoPickerDelegate, PreviewAttachmentImageVideoDele
         }
     }
     
+    func loadAnimatedMedia(from provider: NSItemProvider, completion: @escaping (Data?, Bool) -> Void) {
+        // First: real GIF
+        if provider.hasItemConformingToTypeIdentifier("com.compuserve.gif") {
+            provider.loadFileRepresentation(forTypeIdentifier: "com.compuserve.gif") { url, error in
+                if let url = url, let data = try? Data(contentsOf: url) {
+                    completion(data, true) // true = isGIF
+                } else {
+                    // fallback
+                    self.loadQuickTimeMovie(from: provider, completion: completion)
+                }
+            }
+        } else {
+            // fallback directly
+            self.loadQuickTimeMovie(from: provider, completion: completion)
+        }
+    }
+
+    private func loadQuickTimeMovie(from provider: NSItemProvider, completion: @escaping (Data?, Bool) -> Void) {
+        if provider.hasItemConformingToTypeIdentifier("com.apple.quicktime-movie") {
+            provider.loadFileRepresentation(forTypeIdentifier: "com.apple.quicktime-movie") { url, error in
+                if let url = url, let data = try? Data(contentsOf: url) {
+                    completion(data, false) // false = it's MOV, not GIF
+                } else {
+                    completion(nil, false)
+                }
+            }
+        } else {
+            completion(nil, false)
+        }
+    }
+    
     func sendChatFromPreviewImage(message_text: String, attachment_flag: String, image_id: String, video_id: String, thumb_id: String, gif_id: String, viewController: UIViewController, specFile: String) {
         specFileString = specFile
         sendChat(message_text: message_text, attachment_flag: attachment_flag, image_id: image_id, video_id: video_id, thumb_id: thumb_id, viewController: viewController, gif_id: gif_id)
@@ -3847,21 +3961,16 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
             if (dataMessages[indexPath!.row]["f_pin"]  as? String ?? "") == idMe {
                 children.insert(info, at: children.count - 1)
             }
-            if !(dataMessages[indexPath!.row][TypeDataMessage.message_text]  as? String ?? "").isEmpty {
-                if (dataMessages[indexPath!.row]["f_pin"]  as? String ?? "") == idMe {
-                    children.insert(edit, at: children.count - 1)
-                }
-                if !(dataMessages[indexPath!.row][TypeDataMessage.message_text]  as? String ?? "").isEmpty {
-                    if (dataMessages[indexPath!.row]["f_pin"]  as? String ?? "") == idMe && ((dataMessages[indexPath!.row][TypeDataMessage.is_forwarded] as? Int) ?? 0) == 0 {
-                        let date = Date(milliseconds: Int64(dataMessages[indexPath!.row][TypeDataMessage.server_date] as? String ?? "") ?? 0)
-                        let pastDate = date.addingTimeInterval(-10 * 60)
-                        let differenceInSeconds = date.timeIntervalSince(pastDate)
-                        if abs(differenceInSeconds) <= 15 * 60 {
-                            children.insert(edit, at: children.count - 1)
-                        }
+            if !(dataMessages[indexPath!.row][TypeDataMessage.message_text]  as? String ?? "").isEmpty && (dataMessages[indexPath!.row][TypeDataMessage.attachment_flag] as? String ?? "") != "11" {
+                if (dataMessages[indexPath!.row]["f_pin"]  as? String ?? "") == idMe && ((dataMessages[indexPath!.row][TypeDataMessage.is_forwarded] as? Int) ?? 0) == 0 {
+                    let date = Date(milliseconds: Int64(dataMessages[indexPath!.row][TypeDataMessage.server_date] as? String ?? "") ?? 0)
+                    let pastDate = date.addingTimeInterval(-10 * 60)
+                    let differenceInSeconds = date.timeIntervalSince(pastDate)
+                    if abs(differenceInSeconds) <= 15 * 60 {
+                        children.insert(edit, at: children.count - 1)
                     }
-                    isMore = true
                 }
+                isMore = true
             }
         }
         
@@ -4725,6 +4834,8 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
                     self.timerCredential.removeValue(forKey: dataMessages[i]["message_id"]  as? String ?? "")
                 }
             }
+            let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+            self.pinAllMessages(dataMessages: dataMessagesPin)
             NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
             cancelAction()
         }
@@ -5176,7 +5287,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
         messageText.textDragInteraction?.isEnabled = false
         containerMessage.addSubview(messageText)
         messageText.translatesAutoresizingMaskIntoConstraints = false
-        let topMarginText = messageText.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 32)
+        var topMarginText = messageText.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 32)
         
         let dataProfile = getDataProfile(f_pin: dataMessages[indexPath.row]["f_pin"]  as? String ?? "", message_id: dataMessages[indexPath.row]["message_id"]  as? String ?? "")
         
@@ -5274,7 +5385,10 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                 statusMessage.trailingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: -8).isActive = true
                 statusMessage.widthAnchor.constraint(equalToConstant: 15).isActive = true
                 statusMessage.heightAnchor.constraint(equalToConstant: 15).isActive = true
-                let status = getRealStatus(messageId: dataMessages[indexPath.row]["message_id"]  as? String ?? "")
+                var status = getRealStatus(messageId: dataMessages[indexPath.row]["message_id"]  as? String ?? "")
+                if status == "-1" {
+                    status = dataMessages[indexPath.row]["status"]! as? String ?? ""
+                }
                 if status == "0" {
                     statusMessage.image = UIImage(systemName: "xmark.circle")!.withTintColor(UIColor.red, renderingMode: .alwaysOriginal)
                 } else if status == "1" {
@@ -6223,6 +6337,8 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
             let data = queryMessageReply(message_id: reffChat)
             if (reffChat.isEmpty || data.count == 0) && (dataMessages[indexPath.row][TypeDataMessage.is_forwarded] == nil || dataMessages[indexPath.row][TypeDataMessage.is_forwarded] as! Int == 0) {
                 containerViewFile.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 37).isActive = true
+            } else {
+                containerViewFile.heightAnchor.constraint(greaterThanOrEqualToConstant: 50).isActive = true
             }
             containerViewFile.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
             containerViewFile.bottomAnchor.constraint(equalTo:messageText.topAnchor, constant: -5).isActive = true
@@ -6493,6 +6609,10 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                 containerReply.layer.cornerRadius = 5
                 containerReply.clipsToBounds = true
                 
+                if (thumbChat != "" || fileChat != "") && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"]  as? String ?? "" != "1") {
+                    topMarginText = messageText.topAnchor.constraint(greaterThanOrEqualTo: containerMessage.topAnchor, constant: topMarginText.constant + 50 + (self.offset()*3))
+                }
+                
                 let leftReply = UIView()
                 containerReply.addSubview(leftReply)
                 leftReply.translatesAutoresizingMaskIntoConstraints = false
@@ -6929,9 +7049,13 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
             imageViewer.navigationItem.leftBarButtonItem = backButton
             if Nexilis.checkingAccess(key: "secure_folder_share") || sender.specFile.contains("download") || sender.specFile.contains("share") {
                 let shareAction = UIAction { _ in
-                    var activityViewController = UIActivityViewController(activityItems: [image ?? UIImage()], applicationActivities: nil)
+                    var activityViewController = UIActivityViewController(activityItems: [""], applicationActivities: nil)
                     if type == 1 {
                         activityViewController = UIActivityViewController(activityItems: [url ?? URL(string: "")!], applicationActivities: nil)
+                    } else {
+                        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("ImageSharedNexilis-\(Date().currentTimeMillis())" + ".jpeg")
+                        try? data!.write(to: tempURL)
+                        activityViewController = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil)
                     }
                     activityViewController.popoverPresentationController?.sourceView = imageViewer.view
                     imageViewer.present(activityViewController, animated: true, completion: nil)
@@ -7119,7 +7243,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                 if Nexilis.checkingAccess(key: "secure_folder_share") || sender.specFile.contains("download") || sender.specFile.contains("share") {
                     let shareAction = UIAction { _ in
                         let fileManager = FileManager.default
-                        let tempURL = fileManager.temporaryDirectory.appendingPathComponent(sender.labelFile.text ?? "")
+                        let tempURL = fileManager.temporaryDirectory.appendingPathComponent(urlFile.lastPathComponent)
                         do {
                             if !fileManager.fileExists(atPath: tempURL.path) {
                                 try fileManager.copyItem(at: urlFile, to: tempURL)

+ 147 - 15
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -283,6 +283,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         center.addObserver(self, selector: #selector(onFailedSendMessage(notification:)), name: NSNotification.Name(rawValue: Nexilis.failedSendMessage), object: nil)
         center.addObserver(self, selector: #selector(onRefreshCallLog(notification:)), name: NSNotification.Name(rawValue: "refreshCallLog"), object: nil)
         center.addObserver(self, selector: #selector(onUpdatedMessage(notification:)), name: NSNotification.Name(rawValue: "onUpdatedMessage"), object: nil)
+        center.addObserver(self, selector: #selector(onCheckNewMessages(notification:)), name: NSNotification.Name(rawValue: "checkNewMessagesNexilis"), object: nil)
         
         locationManager.delegate = self
         locationManager.requestWhenInUseAuthorization()
@@ -1106,15 +1107,15 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         })
     }
     
-    private func getData() {
+    private func getData(offset: Int64 = 0) {
 //        let queryCount = "SELECT COUNT(*) FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '3' OR message_scope_id = '18') AND is_call_center = 0"
 //        var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '3' OR message_scope_id = '18') AND is_call_center = 0 order by server_date asc LIMIT CASE WHEN (\(queryCount))-\(dataMessages.count)>=20 THEN 20 ELSE (\(queryCount))-\(dataMessages.count) END OFFSET CASE WHEN (\(queryCount))>=\(20*multipleOffsetUp) THEN (\(queryCount))-\(20*multipleOffsetUp) ELSE 0 END"
-        var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, is_call_center, call_center_id, opposite_pin, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '\(MessageScope.WHISPER)' OR message_scope_id = '\(MessageScope.FORM)' OR message_scope_id = '\(MessageScope.CALL)' OR message_scope_id = '\(MessageScope.MISSED_CALL)') AND is_call_center = 0 order by server_date asc"
+        var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, is_call_center, call_center_id, opposite_pin, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '\(MessageScope.WHISPER)' OR message_scope_id = '\(MessageScope.FORM)' OR message_scope_id = '\(MessageScope.CALL)' OR message_scope_id = '\(MessageScope.MISSED_CALL)') AND is_call_center = 0 order by server_date asc LIMIT -1 OFFSET \(offset)"
         if isContactCenter {
             if complaintId.isEmpty {
-                query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND message_scope_id = '\(MessageScope.CHATROOM)' AND broadcast_flag = 0 AND is_call_center = 1 order by server_date asc"
+                query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND message_scope_id = '\(MessageScope.CHATROOM)' AND broadcast_flag = 0 AND is_call_center = 1 order by server_date asc LIMIT -1 OFFSET \(offset)"
             } else {
-                query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared FROM MESSAGE where message_scope_id = '\(MessageScope.CHATROOM)' AND broadcast_flag = 0 AND is_call_center = 1 AND call_center_id = '\(complaintId)' order by server_date asc"
+                query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared FROM MESSAGE where message_scope_id = '\(MessageScope.CHATROOM)' AND broadcast_flag = 0 AND is_call_center = 1 AND call_center_id = '\(complaintId)' order by server_date asc LIMIT -1 OFFSET \(offset)"
             }
             if isRequestContactCenter && !isDirectCC {
                 viewButton.isHidden = true
@@ -1148,6 +1149,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             do {
                 if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
                     var tempImages: [ImageGrouping] = []
+                    var idxOff = 0
                     while cursorData.next() {
                         var row: [String: Any?] = [:]
                         row["message_id"] = cursorData.string(forColumnIndex: 0)
@@ -1284,7 +1286,11 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                         } else if tempImages.count != 0 {
                             tempImages.removeAll()
                         }
+                        if offset > 0 && idxOff == 0 {
+                            self.markerCounter = row["message_id"] as? String
+                        }
                         dataMessages.append(row)
+                        idxOff+=1
                     }
                     if tempImages.count >= 4 {
                         if tempImages.count > 30 {
@@ -1471,6 +1477,50 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         updateProgress(data)
     }
     
+    @objc func  onCheckNewMessages(notification: NSNotification) {
+        var query = "SELECT COUNT(*) AS total FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '\(MessageScope.WHISPER)' OR message_scope_id = '\(MessageScope.FORM)' OR message_scope_id = '\(MessageScope.CALL)' OR message_scope_id = '\(MessageScope.MISSED_CALL)') AND is_call_center = 0 order by server_date asc"
+        if isContactCenter {
+            if complaintId.isEmpty {
+                query = "SELECT COUNT(*) AS total FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND message_scope_id = '\(MessageScope.CHATROOM)' AND broadcast_flag = 0 AND is_call_center = 1 order by server_date asc"
+            } else {
+                query = "SELECT COUNT(*) AS total FROM MESSAGE where message_scope_id = '\(MessageScope.CHATROOM)' AND broadcast_flag = 0 AND is_call_center = 1 AND call_center_id = '\(complaintId)' order by server_date asc"
+            }
+        }
+        var countMessagesNow: Int64 = 0
+        DispatchQueue.main.async { [self] in
+            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                do {
+                    if let cursorCount = Database.shared.getRecords(fmdb: fmdb, query: query), cursorCount.next() {
+                        countMessagesNow = Int64(cursorCount.int(forColumnIndex: 0))
+                        cursorCount.close()
+                    }
+                }catch{}
+            })
+            if dataMessages.count < countMessagesNow {
+                self.counter = Int(countMessagesNow) - dataMessages.count
+                getData(offset: Int64(self.dataMessages.count))
+                tableChatView.reloadData()
+                if !self.indicatorCounterBSTB.isDescendant(of: self.view) && !self.buttonScrollToBottom.isDescendant(of: self.view) {
+                    let indexMessage = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == self.markerCounter })
+                    if indexMessage != nil {
+                        let section = self.dataDates.firstIndex(of: self.dataMessages[indexMessage!]["chat_date"]  as? String ?? "")
+                        let row = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == self.dataMessages[indexMessage!]["chat_date"]  as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == self.dataMessages[indexMessage!]["message_id"] as? String })
+                        self.tableChatView.scrollToRow(at: IndexPath(row: row!, section: section!), at: .top, animated: true)
+                    }
+                } else if self.buttonScrollToBottom.isDescendant(of: self.view) {
+                    if !self.indicatorCounterBSTB.isDescendant(of: self.view) {
+                        addCounterAtButttonScrollToBottom()
+                    } else {
+                        self.labelCounter.text = "\(counter)"
+                    }
+                } else {
+                    addButtonScrollToBottom()
+                    addCounterAtButttonScrollToBottom()
+                }
+            }
+        }
+    }
+    
     @objc func onUpdatedMessage(notification: NSNotification) {
         DispatchQueue.main.async {
             let data:[AnyHashable : Any] = notification.userInfo!
@@ -3728,9 +3778,12 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                 }
             }
         }
-        if counter == 0 && indicatorCounterBSTB.isDescendant(of: self.view) {
-            indicatorCounterBSTB.removeConstraints(indicatorCounterBSTB.constraints)
-            indicatorCounterBSTB.removeFromSuperview()
+        if counter == 0 {
+            if indicatorCounterBSTB.isDescendant(of: self.view) {
+                indicatorCounterBSTB.removeConstraints(indicatorCounterBSTB.constraints)
+                indicatorCounterBSTB.removeFromSuperview()
+            }
+            updateCounter(counter: 0)
         } else if counter != 0 && currentIndexpath != nil {
             let dataFilter = dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[currentIndexpath!.section] })
             if dataFilter.count == 0 {
@@ -3784,10 +3837,46 @@ extension EditorPersonal: PreviewAttachmentImageVideoDelegate, PHPickerViewContr
             picker.dismiss(animated: true, completion: {
                 Nexilis.showLoader(text: "Preparing...".localized())
                 result.itemProvider.loadDataRepresentation(forTypeIdentifier: "com.compuserve.gif") { data, error in
-                    if let error = error {
-                        print("Error loading GIF: \(error.localizedDescription)")
-                        Nexilis.hideLoader() {
-                            
+                    if error != nil {
+                        self.loadAnimatedMedia(from: result.itemProvider) { data, isGIF in
+                            guard let data = data else {
+                                print("Failed to load media")
+                                return
+                            }
+
+                            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
+                                    }
+                                    if isGIF {
+                                        previewImageVC.fromCopy = true
+                                        previewImageVC.isGIF = true
+                                        previewImageVC.dataGIF = data
+                                        previewImageVC.modalPresentationStyle = .custom
+                                        previewImageVC.delegate = self
+                                        previewImageVC.isAck = self.isAck
+                                        previewImageVC.isConfidential = self.isConfidential
+                                    } else {
+                                        let fileManager = FileManager.default
+                                        let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+                                        let destinationURL = documentsDirectory.appendingPathComponent(UUID().uuidString + ".mov")
+                                        do {
+                                            try data.write(to: destinationURL)
+                                            previewImageVC.modalPresentationStyle = .custom
+                                            previewImageVC.urlVideoPhpPicker = destinationURL
+                                            previewImageVC.delegate = self
+                                            previewImageVC.isAck = self.isAck
+                                            previewImageVC.isConfidential = self.isConfidential
+                                            previewImageVC.isCC = self.isContactCenter
+                                        } catch {
+                                            
+                                        }
+                                    }
+                                    self.present(previewImageVC, animated: true, completion: nil)
+                                }
+                            }
                         }
                     } else if let data = data {
                         DispatchQueue.main.async {
@@ -3879,6 +3968,37 @@ extension EditorPersonal: PreviewAttachmentImageVideoDelegate, PHPickerViewContr
         }
     }
     
+    func loadAnimatedMedia(from provider: NSItemProvider, completion: @escaping (Data?, Bool) -> Void) {
+        // First: real GIF
+        if provider.hasItemConformingToTypeIdentifier("com.compuserve.gif") {
+            provider.loadFileRepresentation(forTypeIdentifier: "com.compuserve.gif") { url, error in
+                if let url = url, let data = try? Data(contentsOf: url) {
+                    completion(data, true) // true = isGIF
+                } else {
+                    // fallback
+                    self.loadQuickTimeMovie(from: provider, completion: completion)
+                }
+            }
+        } else {
+            // fallback directly
+            self.loadQuickTimeMovie(from: provider, completion: completion)
+        }
+    }
+
+    private func loadQuickTimeMovie(from provider: NSItemProvider, completion: @escaping (Data?, Bool) -> Void) {
+        if provider.hasItemConformingToTypeIdentifier("com.apple.quicktime-movie") {
+            provider.loadFileRepresentation(forTypeIdentifier: "com.apple.quicktime-movie") { url, error in
+                if let url = url, let data = try? Data(contentsOf: url) {
+                    completion(data, false) // false = it's MOV, not GIF
+                } else {
+                    completion(nil, false)
+                }
+            }
+        } else {
+            completion(nil, false)
+        }
+    }
+    
     func sendChatFromPreviewImage(message_text: String, attachment_flag: String, image_id: String, video_id: String, thumb_id: String, gif_id: String, viewController: UIViewController, specFile: String) {
         specFileString = specFile
         sendChat(message_text: message_text, attachment_flag: attachment_flag, image_id: image_id, video_id: video_id, thumb_id: thumb_id, viewController: viewController, gif_id : gif_id)
@@ -4982,7 +5102,7 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
             if (dataMessages[indexPath!.row]["f_pin"]  as? String ?? "") == idMe {
                 children.insert(info, at: children.count - 1)
             }
-            if !(dataMessages[indexPath!.row][TypeDataMessage.message_text]  as? String ?? "").isEmpty {
+            if !(dataMessages[indexPath!.row][TypeDataMessage.message_text]  as? String ?? "").isEmpty && (dataMessages[indexPath!.row][TypeDataMessage.attachment_flag] as? String ?? "") != "11" {
                 if (dataMessages[indexPath!.row]["f_pin"]  as? String ?? "") == idMe && ((dataMessages[indexPath!.row][TypeDataMessage.is_forwarded] as? Int) ?? 0) == 0 {
                     let date = Date(milliseconds: Int64(dataMessages[indexPath!.row][TypeDataMessage.server_date] as? String ?? "") ?? 0)
                     let pastDate = date.addingTimeInterval(-10 * 60)
@@ -5725,6 +5845,8 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
                     self.timerCredential.removeValue(forKey: dataMessages[i]["message_id"]  as? String ?? "")
                 }
             }
+            let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+            self.pinAllMessages(dataMessages: dataMessagesPin)
             NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
             cancelAction()
         }
@@ -6703,7 +6825,7 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
         messageText.textDragInteraction?.isEnabled = false
         containerMessage.addSubview(messageText)
         messageText.translatesAutoresizingMaskIntoConstraints = false
-        let topMarginText = messageText.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 15)
+        var topMarginText = messageText.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 15)
         messageText.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
         messageText.font = .systemFont(ofSize: 12 + offset())
         var textChat = (dataMessages[indexPath.row]["message_text"] as? String) ?? ""
@@ -7565,6 +7687,8 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
             let data = queryMessageReply(message_id: reffChat)
             if (reffChat.isEmpty || data.count == 0) && (dataMessages[indexPath.row][TypeDataMessage.is_forwarded] == nil || dataMessages[indexPath.row][TypeDataMessage.is_forwarded] as! Int == 0) {
                 containerViewFile.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 15).isActive = true
+            } else {
+                containerViewFile.heightAnchor.constraint(equalToConstant: 50).isActive = true
             }
             containerViewFile.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
             containerViewFile.bottomAnchor.constraint(equalTo:messageText.topAnchor, constant: -5).isActive = true
@@ -7830,6 +7954,10 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 containerReply.layer.cornerRadius = 5
                 containerReply.clipsToBounds = true
                 
+                if (thumbChat != "" || fileChat != "") && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"]  as? String ?? "" != "1") {
+                    topMarginText = messageText.topAnchor.constraint(greaterThanOrEqualTo: containerMessage.topAnchor, constant: topMarginText.constant + 50 + (self.offset()*3))
+                }
+                
                 let leftReply = UIView()
                 containerReply.addSubview(leftReply)
                 leftReply.translatesAutoresizingMaskIntoConstraints = false
@@ -8339,9 +8467,13 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
             imageViewer.navigationItem.leftBarButtonItem = backButton
             if Nexilis.checkingAccess(key: "secure_folder_share") || sender.specFile.contains("download") || sender.specFile.contains("share") {
                 let shareAction = UIAction { _ in
-                    var activityViewController = UIActivityViewController(activityItems: [image ?? UIImage()], applicationActivities: nil)
+                    var activityViewController = UIActivityViewController(activityItems: [""], applicationActivities: nil)
                     if type == 1 {
                         activityViewController = UIActivityViewController(activityItems: [url ?? URL(string: "")!], applicationActivities: nil)
+                    } else {
+                        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("ImageSharedNexilis-\(Date().currentTimeMillis())" + ".jpeg")
+                        try? data!.write(to: tempURL)
+                        activityViewController = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil)
                     }
                     activityViewController.popoverPresentationController?.sourceView = imageViewer.view
                     imageViewer.present(activityViewController, animated: true, completion: nil)
@@ -8551,7 +8683,7 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 if Nexilis.checkingAccess(key: "secure_folder_share") || sender.specFile.contains("download") || sender.specFile.contains("share") {
                     let shareAction = UIAction { _ in
                         let fileManager = FileManager.default
-                        let tempURL = fileManager.temporaryDirectory.appendingPathComponent(sender.labelFile.text ?? "")
+                        let tempURL = fileManager.temporaryDirectory.appendingPathComponent(urlFile.lastPathComponent)
                         do {
                             if !fileManager.fileExists(atPath: tempURL.path) {
                                 try fileManager.copyItem(at: urlFile, to: tempURL)

+ 6 - 2
NexilisLite/NexilisLite/Source/View/Chat/EditorStarMessages.swift

@@ -1114,9 +1114,13 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
             imageViewer.navigationItem.leftBarButtonItem = backButton
             if Nexilis.checkingAccess(key: "secure_folder_share") || sender.specFile.contains("download") || sender.specFile.contains("share") {
                 let shareAction = UIAction { _ in
-                    var activityViewController = UIActivityViewController(activityItems: [image ?? UIImage()], applicationActivities: nil)
+                    var activityViewController = UIActivityViewController(activityItems: [""], applicationActivities: nil)
                     if type == 1 {
                         activityViewController = UIActivityViewController(activityItems: [url ?? URL(string: "")!], applicationActivities: nil)
+                    } else {
+                        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("ImageSharedNexilis-\(Date().currentTimeMillis())" + ".jpeg")
+                        try? data!.write(to: tempURL)
+                        activityViewController = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil)
                     }
                     activityViewController.popoverPresentationController?.sourceView = imageViewer.view
                     imageViewer.present(activityViewController, animated: true, completion: nil)
@@ -1301,7 +1305,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                 if Nexilis.checkingAccess(key: "secure_folder_share") || sender.specFile.contains("download") || sender.specFile.contains("share") {
                     let shareAction = UIAction { _ in
                         let fileManager = FileManager.default
-                        let tempURL = fileManager.temporaryDirectory.appendingPathComponent(sender.labelFile.text ?? "")
+                        let tempURL = fileManager.temporaryDirectory.appendingPathComponent(urlFile.lastPathComponent)
                         do {
                             if !fileManager.fileExists(atPath: tempURL.path) {
                                 try fileManager.copyItem(at: urlFile, to: tempURL)

+ 3 - 3
NexilisLite/NexilisLite/Source/View/Chat/PreviewAttachmentImageVideo.swift

@@ -544,8 +544,8 @@ class PreviewAttachmentImageVideo: UIViewController, UIScrollViewDelegate, UITex
                     cursor.close()
                 }
                 listMentionWithText.removeAll(where: { listMentionInTextField.contains($0) })
-                var nowTableMention = tableMention
-                var nowHeightTableMention = heightTableMention!
+                let nowTableMention = tableMention
+                let nowHeightTableMention = heightTableMention!
                 if listMentionWithText.count > 0 {
                     if listMentionWithText.count < 5 {
                         nowHeightTableMention.constant = CGFloat(44 * listMentionWithText.count)
@@ -927,7 +927,7 @@ extension PreviewAttachmentImageVideo: UITableViewDelegate, UITableViewDataSourc
     }
     
     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
-        return 2
+        return listMentionWithText.count
     }
     
     func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {