Procházet zdrojové kódy

update progress Whatsapp-like layout : add Call Log VC

alqindiirsyam před 4 měsíci
rodič
revize
d972d2d38f
23 změnil soubory, kde provedl 849 přidání a 91 odebrání
  1. 21 0
      AppBuilder/AppBuilder/Assets.xcassets/tab_6_icon.imageset/Contents.json
  2. binární
      AppBuilder/AppBuilder/Assets.xcassets/tab_6_icon.imageset/tab_6_icon.png
  3. 26 23
      AppBuilder/AppBuilder/SecondTabViewController.swift
  4. 9 3
      AppBuilder/AppBuilder/ViewController.swift
  5. binární
      NexilisLite/.DS_Store
  6. binární
      NexilisLite/NexilisLite/.DS_Store
  7. 11 0
      NexilisLite/NexilisLite/Resource/id.lproj/Localizable.strings
  8. binární
      NexilisLite/NexilisLite/Source/.DS_Store
  9. 4 0
      NexilisLite/NexilisLite/Source/Extension.swift
  10. 2 2
      NexilisLite/NexilisLite/Source/IncomingThread.swift
  11. 33 0
      NexilisLite/NexilisLite/Source/Model/CallModel.swift
  12. 82 13
      NexilisLite/NexilisLite/Source/Nexilis.swift
  13. 2 2
      NexilisLite/NexilisLite/Source/OutgoingThread.swift
  14. 82 0
      NexilisLite/NexilisLite/Source/Utils.swift
  15. 335 0
      NexilisLite/NexilisLite/Source/View/Call/CallLogVC.swift
  16. 25 0
      NexilisLite/NexilisLite/Source/View/Call/QmeraAudioViewController.swift
  17. 28 2
      NexilisLite/NexilisLite/Source/View/Call/QmeraVideoViewController.swift
  18. 2 2
      NexilisLite/NexilisLite/Source/View/Chat/ChatGPTBotView.swift
  19. 12 12
      NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift
  20. 159 26
      NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift
  21. 10 0
      NexilisLite/NexilisLite/Source/View/Contact/ContactCallViewController.swift
  22. 5 5
      NexilisLite/NexilisLite/Source/View/Control/ContactChatViewController.swift
  23. 1 1
      NexilisLite/NexilisLite/Source/View/Control/HistoryBroadcastViewController.swift

+ 21 - 0
AppBuilder/AppBuilder/Assets.xcassets/tab_6_icon.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "tab_6_icon.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

binární
AppBuilder/AppBuilder/Assets.xcassets/tab_6_icon.imageset/tab_6_icon.png


+ 26 - 23
AppBuilder/AppBuilder/SecondTabViewController.swift

@@ -78,7 +78,7 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
         searchController.searchBar.setPositionAdjustment(UIOffset(horizontal: 10, vertical: 0), for: .search)
         searchController.searchBar.setCustomBackgroundImage(image: UIImage(named: self.traitCollection.userInterfaceStyle == .dark ? "nx_search_bar_dark" : "nx_search_bar", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!)
         searchController.obscuresBackgroundDuringPresentation = false
-        searchController.searchBar.searchTextField.attributedPlaceholder = NSAttributedString(string: "Search...".localized(), attributes: [NSAttributedString.Key.foregroundColor: UIColor.gray, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 11)])
+        searchController.searchBar.searchTextField.attributedPlaceholder = NSAttributedString(string: "Search".localized() + "...", attributes: [NSAttributedString.Key.foregroundColor: UIColor.gray, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 11)])
         return searchController
     }()
     
@@ -330,7 +330,7 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
         buttonImageClearSearch.isHidden = true
         buttonImageClearSearch.addTarget(self, action: #selector(clearSearch), for: .touchUpInside)
         
-        textViewSearch.placeholder = "Search...".localized()
+        textViewSearch.placeholder = "Search".localized() + "..."
         textViewSearch.isUserInteractionEnabled = true
         imageViewSearch.addSubview(textViewSearch)
         textViewSearch.font = .systemFont(ofSize: 11 + String.offset())
@@ -622,6 +622,7 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
             segment.setTitle("Chats".localized(), forSegmentAt: 0)
             segment.setTitle("Forums".localized(), forSegmentAt: 1)
         }
+        textViewSearch.placeholder = "Search".localized() + "..."
         if segment.selectedSegmentIndex == 0 {
             Utils.inTabChats = true
         }
@@ -1092,7 +1093,7 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                 smartChatVC.hidesBottomBarWhenPushed = true
                 smartChatVC.fromNotification = false
                 navigationController?.show(smartChatVC, sender: nil)
-            } else if data.messageScope == "3" {
+            } else if data.messageScope == MessageScope.WHISPER || data.messageScope == MessageScope.CALL || data.messageScope == MessageScope.MISSED_CALL {
                 if data.pin.isEmpty {
                     return
                 }
@@ -1775,7 +1776,7 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
             ])
             var leadingAnchor = imageView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 10.0)
             if data.profile.isEmpty && data.pin != "-999" && data.pin != "-997" {
-                if data.messageScope == "3" {
+                if data.messageScope == MessageScope.WHISPER || data.messageScope == MessageScope.CALL || data.messageScope == MessageScope.MISSED_CALL {
                     imageView.image = UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
                 } else {
                     imageView.image = UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
@@ -1823,8 +1824,8 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                         }
                     }
                 } else {
-                    if data.messageScope == "3" || data.isParent || data.pin == "-999" {
-                        getImage(name: data.profile, placeholderImage: UIImage(named: data.pin == "-999" ? "pb_button" : data.messageScope == "3" ? "Profile---Purple" : "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
+                    if data.messageScope == MessageScope.WHISPER || data.messageScope == MessageScope.CALL || data.messageScope == MessageScope.MISSED_CALL || data.isParent || data.pin == "-999" {
+                        getImage(name: data.profile, placeholderImage: UIImage(named: data.pin == "-999" ? "pb_button" : (data.messageScope == MessageScope.WHISPER || data.messageScope == MessageScope.CALL || data.messageScope == MessageScope.MISSED_CALL) ? "Profile---Purple" : "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
                             imageView.image = image
                         })
                     } else {
@@ -1952,24 +1953,26 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                             stringMessage.append(("🚫 _"+"You were deleted this message".localized()+"_").richText())
                         } else {
                             let imageStatus = NSTextAttachment()
-                            let status = getRealStatus(messageId: data.messageId)
-                            if status == "0" {
-                                imageStatus.image = UIImage(systemName: "xmark.circle")!.withTintColor(UIColor.red, renderingMode: .alwaysOriginal)
-                            } else if status == "1" {
-                                imageStatus.image = UIImage(systemName: "clock.arrow.circlepath")!.withTintColor(UIColor.lightGray, renderingMode: .alwaysOriginal)
-                            } else if status == "2" {
-                                imageStatus.image = UIImage(named: "checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.lightGray)
-                            } else if (status == "3") {
-                                imageStatus.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.lightGray)
-                            } else if (status == "8") {
-                                imageStatus.image = UIImage(named: "message_status_ack", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal)
-                            } else {
-                                imageStatus.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.systemBlue)
+                            if data.messageScope != MessageScope.CALL && data.messageScope != MessageScope.MISSED_CALL {
+                                let status = getRealStatus(messageId: data.messageId)
+                                if status == "0" {
+                                    imageStatus.image = UIImage(systemName: "xmark.circle")!.withTintColor(UIColor.red, renderingMode: .alwaysOriginal)
+                                } else if status == "1" {
+                                    imageStatus.image = UIImage(systemName: "clock.arrow.circlepath")!.withTintColor(UIColor.lightGray, renderingMode: .alwaysOriginal)
+                                } else if status == "2" {
+                                    imageStatus.image = UIImage(named: "checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.lightGray)
+                                } else if (status == "3") {
+                                    imageStatus.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.lightGray)
+                                } else if (status == "8") {
+                                    imageStatus.image = UIImage(named: "message_status_ack", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal)
+                                } else {
+                                    imageStatus.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.systemBlue)
+                                }
+                                imageStatus.bounds = CGRect(x: 0, y: -5, width: 15, height: 15)
+                                let imageStatusString = NSAttributedString(attachment: imageStatus)
+                                stringMessage.append(imageStatusString)
+                                stringMessage.append(NSAttributedString(string: " "))
                             }
-                            imageStatus.bounds = CGRect(x: 0, y: -5, width: 15, height: 15)
-                            let imageStatusString = NSAttributedString(attachment: imageStatus)
-                            stringMessage.append(imageStatusString)
-                            stringMessage.append(NSAttributedString(string: " "))
                             if data.messageScope == "4" {
                                 stringMessage.append(NSAttributedString(string: "You".localized() + ": ", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12 + String.offset(), weight: .medium)]))
                             }

+ 9 - 3
AppBuilder/AppBuilder/ViewController.swift

@@ -34,11 +34,13 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
     var secondTab : SecondTabViewController?
     var thirdTab : ThirdTabViewController?
     var fourthTab : FourthTabViewController?
+    var callTab : CallLogVC?
     let emptyTab = EmptyTabViewController()
     public static var isTab1 = true
     public static var isTab2 = false
     public static var isTab3 = false
     public static var isTab4 = false
+    public static var isTabCall = false
     public static var isExpandButton = false
     public static var alwaysHideButton = false
     static var listPullFB: [String] = []
@@ -91,6 +93,7 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
         secondTab = storyboard?.instantiateViewController(withIdentifier: "secondTabVC") as? SecondTabViewController
         thirdTab = storyboard?.instantiateViewController(withIdentifier: "thirdTabVC") as? ThirdTabViewController
         fourthTab = storyboard?.instantiateViewController(withIdentifier: "fourthTabVC") as? FourthTabViewController
+        callTab = CallLogVC()
         
         self.delegate = self
         
@@ -98,6 +101,7 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
         secondTab?.tabBarItem = UITabBarItem(title: "", image: resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "tab_2_icon")!.withTintColor(.white) : UIImage(named: "tab_2_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(named: "tab_2_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor))
         thirdTab?.tabBarItem = UITabBarItem(title: "", image: resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "tab_3_icon")!.withTintColor(.white) : UIImage(named: "tab_3_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(named: "tab_3_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor))
         fourthTab?.tabBarItem = UITabBarItem(title: "", image: resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "tab_4_icon")!.withTintColor(.white) : UIImage(named: "tab_4_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(named: "tab_4_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor))
+        callTab?.tabBarItem = UITabBarItem(title: "", image: resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "tab_6_icon")!.withTintColor(.white) : UIImage(named: "tab_6_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(named: "tab_6_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor))
         var i = 0
         var j = 0
         while j < customTab.count {
@@ -115,6 +119,8 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
                     tabs.append(thirdTab!)
                 case "4":
                     tabs.append(fourthTab!)
+                case "6":
+                    tabs.append(callTab!)
                 default:
                     break
                 }
@@ -142,7 +148,7 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
                     }
                 }
             }
-            if !Utils.getTab2Icon().isEmpty {
+            if !Utils.getTab2Icon().isEmpty && tabs.count > 1 {
                 let urlString = "\(PrefsUtil.getURLBase())get_file_from_path?img=\(Utils.getTab2Icon())"
                 if let cachedImage = ImageCache.shared.image(forKey: urlString) {
                     DispatchQueue.main.async() { [self] in
@@ -161,7 +167,7 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
                     }
                 }
             }
-            if !Utils.getTab3Icon().isEmpty {
+            if !Utils.getTab3Icon().isEmpty && tabs.count > 2 {
                 let urlString = "\(PrefsUtil.getURLBase())get_file_from_path?img=\(Utils.getTab3Icon())"
                 if let cachedImage = ImageCache.shared.image(forKey: urlString) {
                     DispatchQueue.main.async() { [self] in
@@ -188,7 +194,7 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
                     }
                 }
             }
-            if !Utils.getTab4Icon().isEmpty {
+            if !Utils.getTab4Icon().isEmpty && tabs.count > 3 {
                 let urlString = "\(PrefsUtil.getURLBase())get_file_from_path?img=\(Utils.getTab4Icon())"
                 if let cachedImage = ImageCache.shared.image(forKey: urlString) {
                     DispatchQueue.main.async() { [self] in

binární
NexilisLite/.DS_Store


binární
NexilisLite/NexilisLite/.DS_Store


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

@@ -402,3 +402,14 @@
 "has requested to be your friend" = "telah meminta untuk menjadi teman kamu";
 "Friend request has been accepted" = "Permintaan pertemanan telah diterima";
 "Friend request has been rejected" = "Permintaan pertemanan telah ditolak";
+"Calls" = "Panggilan";
+"Recent" = "Terbaru";
+"Incoming" = "Panggilan masuk";
+"Outgoing" = "Panggilan keluar";
+"Missed" = "Panggilan tak terjawab";
+"Audio call" = "Panggilan suara";
+"Video call" = "Panggilan video";
+"Missed audio call" = "Panggilan suara tak terjawab";
+"Missed video call" = "Panggilan video tak terjawab";
+"No answer" = "Tidak ada jawaban";
+"Tap to call back" = "Ketuk untuk menelepon kembali";

binární
NexilisLite/NexilisLite/Source/.DS_Store


+ 4 - 0
NexilisLite/NexilisLite/Source/Extension.swift

@@ -587,6 +587,10 @@ extension UIColor {
         return renderColor(hex: "#4c92d2")
     }
     
+    public static var whatsappGreenColor: UIColor {
+        return renderColor(hex: "#25D366")
+    }
+    
     public class func renderColor(hex: String) -> UIColor {
         var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
 

+ 2 - 2
NexilisLite/NexilisLite/Source/IncomingThread.swift

@@ -640,7 +640,7 @@ class IncomingThread {
                         if let message = cursor.string(forColumnIndex: 0) {
                             //print("MASUK MINQ ADA MESSAGE")
                             OutgoingThread.default.addQueue(message: TMessage(data: message))
-                            if message_scope != "3" {
+                            if message_scope != MessageScope.WHISPER {
                                 DispatchQueue.main.async {
                                     self.updateInquiry(messageId: message_id)
                                 }
@@ -1472,7 +1472,7 @@ class IncomingThread {
             do {
                 if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select * from BUDDY where f_pin = '\(l_pin)'"), cursor.next() {
                     _ = Database.shared.deleteRecord(fmdb: fmdb, table: "BUDDY", _where: "f_pin = '\(l_pin)'")
-                    _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE", _where: "(f_pin='\(l_pin)' or l_pin='\(l_pin)') and (message_scope_id='3' or message_scope_id='18')")
+                    _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE", _where: "(f_pin='\(l_pin)' or l_pin='\(l_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)')")
                     _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", _where: "l_pin='\(l_pin)'")
                     _ = Database.shared.deleteRecord(fmdb: fmdb, table: "POST", _where: "author_f_pin='\(l_pin)'")
                     cursor.close()

+ 33 - 0
NexilisLite/NexilisLite/Source/Model/CallModel.swift

@@ -0,0 +1,33 @@
+//
+//  Call.swift
+//  Pods
+//
+//  Created by Qindi on 09/04/25.
+//
+
+import Foundation
+
+public class CallModel: Model {
+    public var fPin: String
+    public var name: String
+    public var image: String
+    public var time: String
+    public var isVideo: Bool
+    public var status: String
+    public var description: String
+    
+    public init(fPin: String, name: String, image: String, time: String, isVideo: Bool, status: String) {
+        self.fPin = fPin
+        self.name = name
+        self.image = image
+        self.time = time
+        self.isVideo = isVideo
+        self.status = status
+        self.description = ""
+    }
+    
+    public static func == (lhs: CallModel, rhs: CallModel) -> Bool {
+        return lhs.fPin == rhs.fPin
+    }
+    
+}

+ 82 - 13
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -712,7 +712,7 @@ public class Nexilis: NSObject {
 //    }
     
     public static func apiSendChat(destination: String, message: String, isGroup: Bool, thumbnailName: String = "", imageName: String = "", videoName: String = "", fileName: String = "", audioName: String = "", replyMessageId : String = "") -> String {
-        let message = CoreMessage_TMessageBank.sendMessage(l_pin: destination, message_scope_id: isGroup ? "4" : "3", status: "3", message_text: message, credential: "", attachment_flag: !imageName.isEmpty ? "1" : !videoName.isEmpty ? "2" : !audioName.isEmpty ? "5" : !fileName.isEmpty ? "6" : "0", ex_blog_id: "", message_large_text: "", ex_format: "", image_id: imageName, audio_id: audioName, video_id: videoName, file_id: fileName, thumb_id: thumbnailName, reff_id: replyMessageId, read_receipts: "4", chat_id: "", is_call_center: "0", call_center_id: "", opposite_pin: User.getMyPin() ?? "")
+        let message = CoreMessage_TMessageBank.sendMessage(l_pin: destination, message_scope_id: isGroup ? MessageScope.GROUP : MessageScope.WHISPER, status: "3", message_text: message, credential: "", attachment_flag: !imageName.isEmpty ? "1" : !videoName.isEmpty ? "2" : !audioName.isEmpty ? "5" : !fileName.isEmpty ? "6" : "0", ex_blog_id: "", message_large_text: "", ex_format: "", image_id: imageName, audio_id: audioName, video_id: videoName, file_id: fileName, thumb_id: thumbnailName, reff_id: replyMessageId, read_receipts: "4", chat_id: "", is_call_center: "0", call_center_id: "", opposite_pin: User.getMyPin() ?? "")
         addQueueMessage(message: message)
         return message.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID)
     }
@@ -1377,7 +1377,7 @@ public class Nexilis: NSObject {
                     cursor.close()
                 }
                 let l_pin = message.getBody(key : CoreMessage_TMessageKey.L_PIN, default_value : "")
-                let scope = message.getBody(key : CoreMessage_TMessageKey.MESSAGE_SCOPE_ID, default_value : "3")
+                let scope = message.getBody(key : CoreMessage_TMessageKey.MESSAGE_SCOPE_ID, default_value : MessageScope.WHISPER)
                 let status = message.getBody(key : CoreMessage_TMessageKey.STATUS, default_value : "")
                 let chat_id = message.getBody(key : CoreMessage_TMessageKey.CHAT_ID, default_value : "")
                 let broadcast_flag = message.getBody(key: CoreMessage_TMessageKey.BROADCAST_FLAG, default_value: "0")
@@ -1432,7 +1432,7 @@ public class Nexilis: NSObject {
                 
                 if withStatus {
                     do {
-                        if scope == "4" {
+                        if scope == MessageScope.GROUP {
                             for pin in getGroupMembers(fmdb: fmdb, l_pin: l_pin) {
                                 if f_pin == pin { continue }
                                 _ = try Database.shared.insertRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
@@ -1457,7 +1457,7 @@ public class Nexilis: NSObject {
                 }
                 var pin = opposite_pin
                 if pin.isEmpty {
-                    if scope == "4" {
+                    if scope == MessageScope.GROUP {
                         pin = chat_id.isEmpty ? l_pin : chat_id
                     } else {
                         pin = f_pin
@@ -1483,9 +1483,9 @@ public class Nexilis: NSObject {
                 }
                 if is_call_center == "0" {
                     do {
-                        var queryGetLastMessageId = "SELECT message_id FROM MESSAGE where (f_pin = '\(pin)' OR l_pin = '\(pin)') AND message_scope_id = '3' order by server_date desc LIMIT 1"
+                        var queryGetLastMessageId = "SELECT message_id FROM MESSAGE where (f_pin = '\(pin)' OR l_pin = '\(pin)') AND message_scope_id = '\(MessageScope.WHISPER)' order by server_date desc LIMIT 1"
                         if scope == "4" {
-                            queryGetLastMessageId = "SELECT message_id FROM MESSAGE where l_pin = '\(chat_id.isEmpty ? pin : l_pin)' AND chat_id = '\(chat_id)' AND message_scope_id = '4' order by server_date desc LIMIT 1"
+                            queryGetLastMessageId = "SELECT message_id FROM MESSAGE where l_pin = '\(chat_id.isEmpty ? pin : l_pin)' AND chat_id = '\(chat_id)' AND message_scope_id = '\(MessageScope.GROUP)' order by server_date desc LIMIT 1"
                         }
                         var messageId = ""
                         if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: queryGetLastMessageId), cursorData.next() {
@@ -1548,7 +1548,7 @@ public class Nexilis: NSObject {
                     "f_display_name" : "Bot",
                     "l_pin" : me,
                     "l_user_id" : String(user_id!),
-                    "message_scope_id" : "3",
+                    "message_scope_id" : MessageScope.WHISPER,
                     "server_date" : String(Date().currentTimeMillis()),
                     "status" : "3",
                     "message_text" : textMessage,
@@ -1608,6 +1608,75 @@ public class Nexilis: NSObject {
         //print("insert db message summary \(message_id)")
     }
     
+    public static func saveMessageCall(idCall: String, textMessage: String, fPin: String, lPin: String, timeCall: String, attachment_type:String) {
+        guard let me = User.getMyPin() else {
+            return
+        }
+        let dataFpin = User.getDataCanNil(pin: fPin)
+        let dataLpin = User.getDataCanNil(pin: lPin)
+        var messageExist = false
+        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select server_date from MESSAGE where server_date = '\(timeCall)'"), cursor.next() {
+                messageExist = true
+                cursor.close()
+            }
+        })
+        if messageExist {
+            return
+        }
+        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+            do {
+                _ = try Database.shared.insertRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
+                    "message_id" : idCall ,
+                    "f_pin" : fPin,
+                    "f_display_name" : dataFpin != nil ? dataFpin!.fullName : "",
+                    "l_pin" : lPin,
+                    "l_user_id" : dataLpin != nil ? dataLpin!.pin : "",
+                    "message_scope_id" : attachment_type,
+                    "server_date" : timeCall,
+                    "status" : "3",
+                    "message_text" : textMessage,
+                    "audio_id" : "",
+                    "video_id" : "",
+                    "image_id" : "",
+                    "file_id" : "",
+                    "thumb_id" : "",
+                    "opposite_pin" : "",
+                    "format" : "",
+                    "blog_id" : "",
+                    "read_receipts" : "0",
+                    "chat_id" : "",
+                    "account_type" : "1",
+                    "credential" :"",
+                    "reff_id" : "",
+                    "message_large_text" : "",
+                    "attachment_flag" : attachment_type,
+                    "local_timestamp" : timeCall
+                ], replace: true)
+            } catch {
+                rollback.pointee = true
+                print("Access database error: \(error.localizedDescription)")
+            }
+        })
+        let pin = lPin == me ? fPin : lPin
+        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+            do {
+                _ = try Database.shared.insertRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
+                    "l_pin" : pin,
+                    "message_id" : idCall,
+                    "counter" : 0
+                ], replace: true)
+            } catch {
+                rollback.pointee = true
+                print("Access database error: \(error.localizedDescription)")
+            }
+        })
+        var dataMessage: [AnyHashable : Any] = [:]
+        dataMessage["message_id"] = idCall
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "refreshCallLog"), object: nil, userInfo: dataMessage)
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
+    }
+    
     static func updateMessageStatus(message: TMessage) -> Void {
         let message_id = message.getBody(key : CoreMessage_TMessageKey.MESSAGE_ID, default_value : "")
         guard !message_id.isEmpty else {
@@ -3674,7 +3743,7 @@ extension Nexilis: MessageDelegate {
                 } else if !message.getBody(key: videoId).isEmpty {
                     text = "Sent Video 📹"
                 } else if !message.getBody(key: fileId).isEmpty {
-                    if message.getBody(key: messageScopeId) == "18" {
+                    if message.getBody(key: messageScopeId) == MessageScope.FORM {
                         text = "Sent Form 📄"
                     } else {
                         text = "Sent File 📄"
@@ -3704,7 +3773,7 @@ extension Nexilis: MessageDelegate {
 //                if Utils.inTabChats{
 //                    return
 //                }
-                if message.getBody(key: messageScopeId) == "3" || message.getBody(key: messageScopeId) == "18" || message.getBody(key: messageScopeId) == "5" {
+                if message.getBody(key: messageScopeId) == MessageScope.WHISPER || message.getBody(key: messageScopeId) == MessageScope.FORM || message.getBody(key: messageScopeId) == MessageScope.CHATROOM {
                     if inEditorPersonal == sender || (inEditorPersonal != nil && inEditorPersonal!.contains(",")) {
                         return
                     }
@@ -3887,7 +3956,7 @@ extension Nexilis: MessageDelegate {
                                                 showNotif()
                                             }
                                             var soundId: String = SecureUserDefaults.shared.value(forKey: "newNotifSoundPersonal") ?? "001:Nexilis Message (Default)"
-                                            if message.getBody(key: CoreMessage_TMessageKey.MESSAGE_SCOPE_ID) == "4" {
+                                            if message.getBody(key: CoreMessage_TMessageKey.MESSAGE_SCOPE_ID) == MessageScope.GROUP {
                                                 soundId = SecureUserDefaults.shared.value(forKey: "newNotifSoundGroup") ?? "001:Nexilis Message (Default)"
                                             }
                                             do {
@@ -3993,7 +4062,7 @@ extension Nexilis: MessageDelegate {
                             profileImage.contentMode = .scaleAspectFill
                         } else {
                             profileImage.circle()
-                            if message.getBody(key: messageScopeId) == "3" {
+                            if message.getBody(key: messageScopeId) == MessageScope.WHISPER {
                                 profileImage.image = UIImage(systemName: "person")
                             } else {
                                 profileImage.image = UIImage(systemName: "person.3")
@@ -4006,7 +4075,7 @@ extension Nexilis: MessageDelegate {
                         floating.show(queuePosition: .front, bannerPosition: .top, queue: NotificationBannerQueue(maxBannersOnScreenSimultaneously: 1), on: nil, edgeInsets: UIEdgeInsets(top: 8.0, left: 8.0, bottom: 0, right: 8.0), cornerRadius: 8.0, shadowColor: .clear, shadowOpacity: .zero, shadowBlurRadius: .zero, shadowCornerRadius: .zero, shadowOffset: .zero, shadowEdgeInsets: nil)
     //                    let vibrateMode: Bool = SecureUserDefaults.shared.value(forKey: "vibrateMode") ?? false
                         var soundId: String = SecureUserDefaults.shared.value(forKey: "newNotifSoundPersonal") ?? "001:Nexilis Message (Default)"
-                        if message.getBody(key: CoreMessage_TMessageKey.MESSAGE_SCOPE_ID) == "4" {
+                        if message.getBody(key: CoreMessage_TMessageKey.MESSAGE_SCOPE_ID) == MessageScope.GROUP {
                             soundId = SecureUserDefaults.shared.value(forKey: "newNotifSoundGroup") ?? "001:Nexilis Message (Default)"
                         }
                         do {
@@ -4188,7 +4257,7 @@ extension Nexilis: MessageDelegate {
                                 print("Access database error: \(error.localizedDescription)")
                             }
                         })
-                        if message.getBody(key: messageScopeId) == "3" || message.getBody(key: messageScopeId) == "18" || message.getBody(key: messageScopeId) == "5" {
+                        if message.getBody(key: messageScopeId) == MessageScope.WHISPER || message.getBody(key: messageScopeId) == MessageScope.FORM || message.getBody(key: messageScopeId) == MessageScope.CHATROOM {
                             let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
                             editorPersonalVC.hidesBottomBarWhenPushed = true
                             editorPersonalVC.unique_l_pin = threadIdentifier

+ 2 - 2
NexilisLite/NexilisLite/Source/OutgoingThread.swift

@@ -403,9 +403,9 @@ class OutgoingThread {
                         if !chat.isEmpty {
                             pin = chat
                         }
-                        var queryGetLastMessageId = "SELECT message_id FROM MESSAGE where (f_pin = '\(pin)' OR l_pin = '\(pin)') AND message_scope_id = '3' order by server_date desc LIMIT 1"
+                        var queryGetLastMessageId = "SELECT message_id FROM MESSAGE where (f_pin = '\(pin)' OR l_pin = '\(pin)') AND message_scope_id = '\(MessageScope.WHISPER)' order by server_date desc LIMIT 1"
                         if scope == "4" {
-                            queryGetLastMessageId = "SELECT message_id FROM MESSAGE where l_pin = '\(chat.isEmpty ? pin : l_pin)' AND chat_id = '\(chat)' AND message_scope_id = '4' order by server_date desc LIMIT 1"
+                            queryGetLastMessageId = "SELECT message_id FROM MESSAGE where l_pin = '\(chat.isEmpty ? pin : l_pin)' AND chat_id = '\(chat)' AND message_scope_id = '\(MessageScope.GROUP)' order by server_date desc LIMIT 1"
                         }
                         var messageId = ""
                         if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: queryGetLastMessageId), cursorData.next() {

+ 82 - 0
NexilisLite/NexilisLite/Source/Utils.swift

@@ -411,6 +411,56 @@ public final class Utils {
     public static func previewMessageText(chat: Chat) -> Any {
         if chat.credential == "1" && chat.lock == "2" {
             return ("🚫 _"+"Message has expired".localized()+"_").richText(group_id: chat.pin)
+        } else if chat.messageScope == MessageScope.CALL || chat.messageScope == MessageScope.MISSED_CALL {
+            let imageAttachment = NSTextAttachment()
+            var stringImage = ""
+            let isVideo = chat.messageText.lowercased().contains("video")
+            let type = chat.messageText.lowercased().contains("incoming") ? "1" : chat.messageText.lowercased().contains("outgoing") ? "2" : "3"
+            var textPreview = ""
+            if isVideo && type == "2" {
+                stringImage = "arrow.up.right.video.fill"
+                textPreview = "Video call".localized()
+            } else if !isVideo && type == "2" {
+                stringImage = "phone.fill.arrow.up.right"
+                textPreview = "Audio call".localized()
+            } else if isVideo {
+                stringImage = "arrow.down.left.video.fill"
+                textPreview = type == "3" ? "Missed video call".localized() : "Video call".localized()
+            } else {
+                stringImage = "phone.fill.arrow.down.left"
+                textPreview = type == "3" ? "Missed audio call".localized() : "Audio call".localized()
+            }
+            if let image = UIImage(systemName: stringImage)?.withRenderingMode(.alwaysTemplate) {
+                let imageView = UIImageView(image: image)
+                if type == "3" {
+                    imageView.tintColor = .red
+                } else {
+                    imageView.tintColor = .gray
+                }
+                
+                // Render the UIImageView to UIImage with tint applied
+                UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, 0.0)
+                imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
+                let tintedImage = UIGraphicsGetImageFromCurrentImageContext()
+                UIGraphicsEndImageContext()
+                
+                imageAttachment.image = tintedImage
+            }
+
+            let imageSize = CGSize(width: 18, height: 18)
+            imageAttachment.bounds = CGRect(x: 0, y: -2, width: isVideo ? imageSize.width + 8 : imageSize.width, height: imageSize.height)
+
+            let imageString = NSAttributedString(attachment: imageAttachment)
+            let textString = NSAttributedString(string: " " + textPreview, attributes: [
+                .font: UIFont.systemFont(ofSize: 14),
+                .foregroundColor: UIColor.gray
+            ])
+            
+            let finalString = NSMutableAttributedString()
+            finalString.append(imageString)
+            finalString.append(textString)
+            
+            return finalString
         } else if chat.credential == "1" {
             return showNSMutableAttributedString("Confidential Message".localized())
         } else if chat.attachmentFlag == "27" {
@@ -2764,3 +2814,35 @@ public class SecureUserDefaults {
     }
 }
 
+public class MessageScope {
+    public static let GLOBAL = "1";
+    public static let LOCAL = "2";
+    public static let WHISPER = "3";
+    public static let GROUP = "4";
+    public static let CHATROOM = "5";
+    public static let PLACE = "6";
+    public static let BUDDY = "7";
+    public static let FOLLOWER = "8";
+    public static let APP = "9";
+    public static let BLOG = "10";
+    public static let BOT = "11";
+    public static let CALL = "12";
+    public static let QUOTE = "13";
+    public static let DRAW = "14";
+    public static let SMS = "15";
+    public static let EMAIL = "16";
+    public static let LIVE_BRAODCAST = "17";
+    public static let FORM = "18";
+    public static let MISSED_CALL = "19";
+    public static let VIDEO_ATTACHMNET = "20";
+    public static let UNREAD_COUNT = "21";
+    public static let FAVORITE = "22";
+    public static let CALENDAR = "23";
+    public static let PILPRES = "25";
+    public static let CHATBOT = "26";
+    public static let BROADCAST_HISTORY = "30";
+    public static let GPT_CHATBOT = "31";
+    public static let COMMUNITY = "32";
+    public static let CHANNEL = "33";
+}
+

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

@@ -0,0 +1,335 @@
+//
+//  CallFragment.swift
+//  AppBuilder
+//
+//  Created by Qindi on 09/04/25.
+//
+
+import Foundation
+import UIKit
+
+public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchResultsUpdating {
+    private let tableView = UITableView(frame: .zero, style: .plain)
+    private let searchController = UISearchController(searchResultsController: nil)
+        
+    private var calls: [CallModel] = []
+    private let textCallEmpty = UILabel()
+    
+    public override func viewDidLoad() {
+        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellCallLog")
+        tableView.dataSource = self
+        tableView.delegate = self
+        tableView.tableFooterView = UIView()
+        tableView.sectionHeaderHeight = 0
+        tableView.sectionFooterHeight = 0
+        tableView.automaticallyAdjustsScrollIndicatorInsets = false
+        
+        setupTableView()
+        refresh()
+        NotificationCenter.default.addObserver(self, selector: #selector(onRefreshCallLog(notification:)), name: NSNotification.Name(rawValue: "refreshCallLog"), object: nil)
+    }
+    
+    public override func viewWillAppear(_ animated: Bool) {
+        tabBarController?.navigationItem.title = "Calls".localized()
+        tabBarController?.navigationItem.hidesSearchBarWhenScrolling = true
+        navigationController?.setNavigationBarHidden(false, animated: false)
+        navigationController?.navigationBar.prefersLargeTitles = true
+        navigationController?.navigationItem.largeTitleDisplayMode = .always
+        let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.foregroundColor: self.traitCollection.userInterfaceStyle == .dark ? .white : UIColor.black, NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 16)]
+        let largeAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.foregroundColor: self.traitCollection.userInterfaceStyle == .dark ? .white : UIColor.black, NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 34)]
+        let appearance = UINavigationBarAppearance()
+        appearance.configureWithTransparentBackground()
+        appearance.titleTextAttributes = attributes
+        appearance.largeTitleTextAttributes = largeAttributes
+        navigationController?.navigationBar.standardAppearance = appearance
+        navigationController?.navigationBar.scrollEdgeAppearance = appearance
+        
+        let leftButton = UIButton(type: .system)
+        let imageLeft = UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .bold))
+        leftButton.setImage(imageLeft, for: .normal)
+        leftButton.tintColor = .black
+        leftButton.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
+        leftButton.layer.cornerRadius = 15
+        leftButton.clipsToBounds = true
+        leftButton.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
+        leftButton.addTarget(self, action: #selector(leftBarButtonTapped), for: .touchUpInside)
+        let leftBarButtonItem = UIBarButtonItem(customView: leftButton)
+        tabBarController?.navigationItem.leftBarButtonItem = leftBarButtonItem
+        
+        let rightButton = UIButton(type: .system)
+        let imageRight = UIImage(systemName: "plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .bold))
+        rightButton.setImage(imageRight, for: .normal)
+        rightButton.tintColor = .white
+        rightButton.backgroundColor = .whatsappGreenColor
+        rightButton.layer.cornerRadius = 15
+        rightButton.clipsToBounds = true
+        rightButton.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
+        rightButton.addTarget(self, action: #selector(rightBarButtonTapped), for: .touchUpInside)
+        let rightBarButtonItem = UIBarButtonItem(customView: rightButton)
+        tabBarController?.navigationItem.rightBarButtonItem = rightBarButtonItem
+    }
+    
+    private func refresh() {
+        getData()
+        
+        if calls.count > 0 {
+            if textCallEmpty.isDescendant(of: view){
+                textCallEmpty.removeFromSuperview()
+            }
+            searchController.searchResultsUpdater = self
+            searchController.searchBar.searchTextField.attributedPlaceholder = NSAttributedString(string: "Search".localized(), attributes: [NSAttributedString.Key.foregroundColor: UIColor.gray, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)])
+            searchController.obscuresBackgroundDuringPresentation = false
+            searchController.hidesNavigationBarDuringPresentation = true
+
+            tabBarController?.navigationItem.searchController = searchController
+            definesPresentationContext = true
+            
+            tableView.reloadData()
+        } else {
+            textCallEmpty.numberOfLines = 0
+            let fullText = "To place audio or video call, tap ⊕ at the top and select a contact.".localized()
+            let attributedString = NSMutableAttributedString(string: fullText)
+            attributedString.addAttribute(.font, value: UIFont.systemFont(ofSize: 25), range: NSRange(location: 0, length: attributedString.length))
+            if let plusRange = fullText.range(of: "⊕") {
+                let nsRange = NSRange(plusRange, in: fullText)
+                attributedString.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: 40), range: nsRange)
+            }
+            textCallEmpty.attributedText = attributedString
+            
+            view.addSubview(textCallEmpty)
+            textCallEmpty.anchor(left: view.leftAnchor, right: view.rightAnchor, paddingLeft: 20, paddingRight: 20, centerX: view.centerXAnchor, centerY: view.centerYAnchor)
+        }
+        DispatchQueue.main.async {
+            self.navigationController?.navigationBar.sizeToFit()
+        }
+    }
+    
+    @objc func onRefreshCallLog(notification: NSNotification) {
+        DispatchQueue.main.async {
+            self.refresh()
+        }
+    }
+    
+    private func getData() {
+        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, l_pin, message_text, server_date, message_scope_id from MESSAGE where message_scope_id = '\(MessageScope.CALL)' or message_scope_id = '\(MessageScope.MISSED_CALL)' order by server_date desc") {
+                var tempCall: [CallModel] = []
+                while cursor.next() {
+                    let fPin = cursor.string(forColumnIndex: 0) ?? ""
+                    let lPin = cursor.string(forColumnIndex: 1) ?? ""
+                    let text = cursor.string(forColumnIndex: 2) ?? ""
+                    let time = cursor.string(forColumnIndex: 3) ?? ""
+                    let scope = cursor.string(forColumnIndex: 4) ?? ""
+                    let me = User.getMyPin() ?? ""
+                    let pin = fPin == me ? lPin : fPin
+                    let dataPin = User.getDataCanNil(pin: pin, fmdb: fmdb)
+                    var statusCall = "1"
+                    if scope == MessageScope.CALL && fPin == me {
+                        statusCall = "2"
+                    } else if scope == MessageScope.MISSED_CALL {
+                        statusCall = "3"
+                    }
+                    
+                    var timeCall = ""
+                    let date = Date(milliseconds: Int64(time) ?? 0)
+                    let calendar = Calendar.current
+                    
+                    if (calendar.isDateInToday(date)) {
+                        let formatter = DateFormatter()
+                        formatter.dateFormat = "HH:mm"
+                        formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
+                        timeCall = formatter.string(from: date as Date)
+                    } else {
+                        let startOfNow = calendar.startOfDay(for: Date())
+                        let startOfTimeStamp = calendar.startOfDay(for: date)
+                        let components = calendar.dateComponents([.day], from: startOfNow, to: startOfTimeStamp)
+                        let day = -(components.day!)
+                        if day == 1 {
+                            timeCall = "Yesterday".localized()
+                        } else {
+                            if day < 7 {
+                                let formatter = DateFormatter()
+                                formatter.dateFormat = "EEEE"
+                                let lang: String = SecureUserDefaults.shared.value(forKey: "i18n_language") ?? "en"
+                                if lang == "id" {
+                                    formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
+                                }
+                                timeCall = formatter.string(from: date)
+                            } else {
+                                let formatter = DateFormatter()
+                                formatter.dateFormat = "M/dd/yy"
+                                formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
+                                let stringFormat = formatter.string(from: date as Date)
+                                timeCall = stringFormat
+                            }
+                        }
+                    }
+                    if dataPin != nil {
+                        tempCall.append(CallModel(fPin: fPin, name: dataPin!.fullName, image: dataPin!.thumb, time: timeCall, isVideo: text.lowercased().contains("audio") ? false : true, status: statusCall))
+                    }
+                }
+                calls = tempCall
+                cursor.close()
+            }
+        })
+    }
+    
+    @objc func leftBarButtonTapped() {
+        print("Left bar button tapped")
+    }
+    
+    @objc func rightBarButtonTapped() {
+        APIS.openCall()
+    }
+    
+    private func setupTableView() {
+        view.addSubview(tableView)
+        tableView.translatesAutoresizingMaskIntoConstraints = false
+        
+        NSLayoutConstraint.activate([
+            tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+            tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
+            tableView.rightAnchor.constraint(equalTo: view.rightAnchor)
+        ])
+    }
+    
+    public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
+        let header = UIView()
+        
+        if calls.count > 0 {
+            let label = UILabel()
+            label.text = "Recent".localized()
+            label.font = .boldSystemFont(ofSize: 20)
+            label.textColor = .black
+            label.frame = CGRect(x: 20, y: 0, width: tableView.frame.width, height: 40)
+            header.addSubview(label)
+        }
+        
+        return header
+    }
+    
+    public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 40
+    }
+    
+    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        let sectionHeaderHeight: CGFloat = 40
+        if scrollView.contentOffset.y <= sectionHeaderHeight && scrollView.contentOffset.y >= 0 {
+            scrollView.contentInset.top = -scrollView.contentOffset.y
+        } else if scrollView.contentOffset.y >= sectionHeaderHeight {
+            scrollView.contentInset.top = -sectionHeaderHeight
+        }
+    }
+    
+    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return calls.count
+    }
+    
+    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let call = calls[indexPath.row]
+        let cell = tableView.dequeueReusableCell(withIdentifier: "cellCallLog", for: indexPath)
+        let textTime = UILabel()
+        textTime.text = call.time
+        textTime.font = .systemFont(ofSize: 14)
+        textTime.textColor = .gray
+        textTime.textAlignment = .right
+        textTime.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        textTime.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        
+        let detailButton = UIButton(type: .detailDisclosure)
+        detailButton.addTarget(self, action: #selector(detailButtonTapped), for: .touchUpInside)
+
+        let stack = UIStackView(arrangedSubviews: [textTime, detailButton])
+        stack.axis = .horizontal
+        stack.spacing = 8
+        stack.alignment = .center
+        stack.translatesAutoresizingMaskIntoConstraints = false
+        
+        let container = UIView()
+        container.addSubview(stack)
+
+        NSLayoutConstraint.activate([
+            stack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+            stack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+            stack.topAnchor.constraint(equalTo: container.topAnchor),
+            stack.bottomAnchor.constraint(equalTo: container.bottomAnchor)
+        ])
+        container.frame = CGRect(x: 0, y: 0, width: 120, height: 30)
+        
+        cell.accessoryView = container
+        cell.tintColor = .black
+        
+        var content = cell.defaultContentConfiguration()
+        content.text = call.name
+        content.textProperties.font = .systemFont(ofSize: 16)
+        if call.status == "3" {
+            content.textProperties.color = .red
+        }
+        content.secondaryAttributedText = typeCallLog(type: call.status, isVideo: call.isVideo)
+        content.secondaryTextProperties.font = .systemFont(ofSize: 14)
+        content.secondaryTextProperties.color = .gray
+        getImage(name: call.image, placeholderImage: UIImage(systemName: "person.circle.fill"), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
+            content.image = image
+        })
+        let constantSize = 40.0
+        content.imageProperties.tintColor = .lightGray
+        content.imageProperties.maximumSize = CGSize(width: constantSize, height: constantSize)
+        content.imageProperties.reservedLayoutSize = CGSize(width: constantSize, height: constantSize)
+        content.imageProperties.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: constantSize)
+        cell.contentConfiguration = content
+        
+        return cell
+    }
+    
+    @objc func detailButtonTapped() {
+        print("Detail button tapped")
+    }
+    
+    public func updateSearchResults(for searchController: UISearchController) {
+        // Handle search updates here
+        let searchText = searchController.searchBar.text ?? ""
+        print("Searching for: \(searchText)")
+    }
+    
+    private func typeCallLog(type: String, isVideo: Bool) -> NSMutableAttributedString {
+        let imageAttachment = NSTextAttachment()
+        var stringImage = ""
+        if isVideo && type == "2" {
+            stringImage = "arrow.up.right.video.fill"
+        } else if !isVideo && type == "2" {
+            stringImage = "phone.fill.arrow.up.right"
+        } else if isVideo {
+            stringImage = "arrow.down.left.video.fill"
+        } else {
+            stringImage = "phone.fill.arrow.down.left"
+        }
+        if let image = UIImage(systemName: stringImage)?.withRenderingMode(.alwaysTemplate) {
+            let imageView = UIImageView(image: image)
+            imageView.tintColor = .gray
+            
+            // Render the UIImageView to UIImage with tint applied
+            UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, 0.0)
+            imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
+            let tintedImage = UIGraphicsGetImageFromCurrentImageContext()
+            UIGraphicsEndImageContext()
+            
+            imageAttachment.image = tintedImage
+        }
+
+        let imageSize = CGSize(width: 18, height: 18)
+        imageAttachment.bounds = CGRect(x: 0, y: -2, width: isVideo ? imageSize.width + 8 : imageSize.width, height: imageSize.height)
+
+        let imageString = NSAttributedString(attachment: imageAttachment)
+        let textString = NSAttributedString(string: type == "1" ? (" " + "Incoming".localized()) : type == "2" ? (" " + "Outgoing".localized()) : (" " + "Missed".localized()), attributes: [
+            .font: UIFont.systemFont(ofSize: 14),
+            .foregroundColor: UIColor.gray
+        ])
+        
+        let finalString = NSMutableAttributedString()
+        finalString.append(imageString)
+        finalString.append(textString)
+        
+        return finalString
+    }
+}

+ 25 - 0
NexilisLite/NexilisLite/Source/View/Call/QmeraAudioViewController.swift

@@ -31,6 +31,8 @@ class QmeraAudioViewController: UIViewController {
     var wbRoomId = ""
     var callFCM = true
     var autoAcceptAPN = false
+    var timeStartCall = ""
+    var idCall = ""
     
     
     let buttonSize: CGFloat = 70
@@ -414,6 +416,8 @@ class QmeraAudioViewController: UIViewController {
 //                })
             }
         }
+        self.timeStartCall = String(Date().currentTimeMillis())
+        self.idCall = (User.getMyPin() ?? "") + CoreMessage_TMessageUtil.getTID()
     }
     
     override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
@@ -743,6 +747,12 @@ class QmeraAudioViewController: UIViewController {
         alert.addAction(UIAlertAction(title: "No".localized(), style: UIAlertAction.Style.default, handler: nil))
         alert.addAction(UIAlertAction(title: "Yes".localized(), style: UIAlertAction.Style.default, handler: {(_) in
             DispatchQueue.main.async {
+                if self.timer == nil || self.isOutgoing {
+                    let longCall = self.timer == nil ? "0" : self.status.text ?? ""
+                    Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Outgoing audio call".localized() + " at \(longCall)", fPin: User.getMyPin() ?? "", lPin: !self.data.isEmpty ? self.data : self.user != nil ? self.user!.pin : "", timeCall: self.timeStartCall, attachment_type: MessageScope.CALL)
+                } else if !self.isOutgoing {
+                    Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Incoming audio call".localized() + " at \(self.status.text ?? "")", fPin: !self.data.isEmpty ? self.data : self.user != nil ? self.user!.pin : "", lPin: User.getMyPin() ?? "", timeCall: self.timeStartCall, attachment_type: MessageScope.CALL)
+                }
                 self.timer?.invalidate()
                 self.timer = nil
                 self.status.text = "Audio Call Ended".localized()
@@ -886,6 +896,9 @@ class QmeraAudioViewController: UIViewController {
     }
     
     @objc func didReject(sender: Any?) {
+        if self.timer == nil {
+            Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Missed audio call".localized() + " at 0", fPin: !self.data.isEmpty ? self.data : self.user != nil ? self.user!.pin : "", lPin: User.getMyPin() ?? "", timeCall: self.timeStartCall, attachment_type: MessageScope.MISSED_CALL)
+        }
         didEnd(sender: sender)
     }
     
@@ -1082,6 +1095,16 @@ class QmeraAudioViewController: UIViewController {
                         return
                     } else if users.count == 0 {
                         DispatchQueue.main.async {
+                            if self.isOutgoing {
+                                let longCall = self.timer == nil ? "0" : self.status.text ?? ""
+                                Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Outgoing audio call".localized() + " at \(longCall)", fPin: User.getMyPin() ?? "", lPin: !self.data.isEmpty ? self.data : self.user != nil ? self.user!.pin : "", timeCall: self.timeStartCall, attachment_type: MessageScope.CALL)
+                            } else {
+                                if self.timer == nil {
+                                    Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Missed audio call".localized() + " at 0", fPin: !self.data.isEmpty ? self.data : self.user != nil ? self.user!.pin : "", lPin: User.getMyPin() ?? "", timeCall: self.timeStartCall, attachment_type: MessageScope.MISSED_CALL)
+                                } else {
+                                    Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Incoming audio call".localized() + " at \(self.status.text ?? "")", fPin: !self.data.isEmpty ? self.data : self.user != nil ? self.user!.pin : "", lPin: User.getMyPin() ?? "", timeCall: self.timeStartCall, attachment_type: MessageScope.CALL)
+                                }
+                            }
                             self.timer?.invalidate()
                             self.timer = nil
                             self.status.text = "Audio Call Ended".localized()
@@ -1171,6 +1194,8 @@ class QmeraAudioViewController: UIViewController {
                 }
                 if users.count == 0 {
                     DispatchQueue.main.async {
+                        let longCall =  "0"
+                        Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Outgoing audio call".localized() + " at \(longCall)", fPin: User.getMyPin() ?? "", lPin: !self.data.isEmpty ? self.data : self.user != nil ? self.user!.pin : "", timeCall: self.timeStartCall, attachment_type: MessageScope.CALL)
                         self.status.text = "Busy..."
                         self.end.isEnabled = false
                         if self.isOutgoing {

+ 28 - 2
NexilisLite/NexilisLite/Source/View/Call/QmeraVideoViewController.swift

@@ -97,6 +97,8 @@ class QmeraVideoViewController: UIViewController {
     var isAddCall = ""
     var ticketId = ""
     var autoAcceptAPN = false
+    var timeStartCall = ""
+    var idCall = ""
     private var frontCamera = true
     var users: [User] = []
     let poweredByView: UIStackView = {
@@ -248,7 +250,8 @@ class QmeraVideoViewController: UIViewController {
                 }
             }
         }
-        
+        self.timeStartCall = String(Date().currentTimeMillis())
+        self.idCall = (User.getMyPin() ?? "") + CoreMessage_TMessageUtil.getTID()
     }
     
     override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
@@ -483,7 +486,7 @@ class QmeraVideoViewController: UIViewController {
                         if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getCalling(fPin: self.dataPerson[0]["f_pin"]!!, type: "2"), timeout: 30 * 1000) {
                             if response.isOk() {
                                 
-                            } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "01" {
+                            } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "01" && self.dataPerson.count > 0 {
                                 API.initiateCCall(sParty: self.dataPerson[0]["f_pin"]!, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: self.listRemoteViewFix, ivLocalView: self.cameraView, ivRemoteZ: self.zoomView)
                             } else {
                                 DispatchQueue.main.async {
@@ -613,6 +616,22 @@ class QmeraVideoViewController: UIViewController {
         }
     }
     
+    private func makeStateCall() {
+        var longCall = "0"
+        if self.vcTimer.isValid {
+            longCall = self.labelTimerVC.text ?? ""
+        }
+        if self.isInisiator {
+            Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Outgoing video call" + " at \(longCall)", fPin: User.getMyPin() ?? "", lPin: self.dataPerson[0]["f_pin"]!!, timeCall: self.timeStartCall, attachment_type: MessageScope.CALL)
+        } else {
+            if !self.vcTimer.isValid {
+                Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Missed video call" + " at 0", fPin: self.dataPerson[0]["f_pin"]!!, lPin: User.getMyPin() ?? "", timeCall: self.timeStartCall, attachment_type: MessageScope.MISSED_CALL)
+            } else {
+                Nexilis.saveMessageCall(idCall: self.idCall, textMessage: "Incoming video call" + " at \(longCall)", fPin: self.dataPerson[0]["f_pin"]!!, lPin: User.getMyPin() ?? "", timeCall: self.timeStartCall, attachment_type: MessageScope.CALL)
+            }
+        }
+    }
+    
     @objc func didTapDeclineCallButton(sender: AnyObject){
         let onGoingCC: String = SecureUserDefaults.shared.value(forKey: "onGoingCC") ?? ""
         if !onGoingCC.isEmpty {
@@ -636,6 +655,7 @@ class QmeraVideoViewController: UIViewController {
             let alert = LibAlertController(title: "End Video Call".localized(), message: "Are you sure you want to end video call?".localized(), preferredStyle: .alert)
             alert.addAction(UIAlertAction(title: "No".localized(), style: UIAlertAction.Style.default, handler: nil))
             alert.addAction(UIAlertAction(title: "Yes".localized(), style: UIAlertAction.Style.default, handler: {(_) in
+                self.makeStateCall()
                 Nexilis.stopRingtoneCall()
                 Nexilis.stopRingbacktoneCall()
 //                if self.labelIncomingOutgoing.isDescendant(of: self.view) {
@@ -1392,6 +1412,11 @@ class QmeraVideoViewController: UIViewController {
             }
             DispatchQueue.main.async {
                 if (self.dataPerson.count == 1) {
+                    var longCall = "0"
+                    if self.vcTimer.isValid {
+                        longCall = self.labelTimerVC.text ?? ""
+                    }
+                    self.makeStateCall()
 //                    if self.labelIncomingOutgoing.isDescendant(of: self.view) {
 //                        self.labelIncomingOutgoing.text = "Video call is over".localized()
 //                    }
@@ -1569,6 +1594,7 @@ class QmeraVideoViewController: UIViewController {
             let onGoingCC: String = SecureUserDefaults.shared.value(forKey: "onGoingCC") ?? ""
             DispatchQueue.main.async { [self] in
                 if (self.dataPerson.count == 1) {
+                    self.makeStateCall()
                     if self.labelIncomingOutgoing.isDescendant(of: self.view) {
                         self.labelIncomingOutgoing.text = "Busy".localized()
                     }

+ 2 - 2
NexilisLite/NexilisLite/Source/View/Chat/ChatGPTBotView.swift

@@ -287,7 +287,7 @@ public class ChatGPTBotView: UIViewController, UIGestureRecognizerDelegate {
 //        }
     }
     
-    private func sendChat(message_scope_id:String =  "31", status:String =  "4", 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 = "4", chat_id: String = "", is_call_center: String = "0", call_center_id: String = "", viewController: UIViewController) {
+    private func sendChat(message_scope_id:String =  MessageScope.GPT_CHATBOT, status:String =  "4", 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 = "4", chat_id: String = "", is_call_center: String = "0", call_center_id: String = "", viewController: UIViewController) {
         if viewController is ChatGPTBotView {
             if ((textFieldSend.text!.trimmingCharacters(in: .whitespacesAndNewlines) == "Send message".localized() && textFieldSend.textColor == UIColor.lightGray && attachment_flag != "11") || textFieldSend.text!.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ) {
                 dismissKeyboard()
@@ -599,7 +599,7 @@ public class ChatGPTBotView: UIViewController, UIGestureRecognizerDelegate {
     }
     
     private func getData() {
-        let 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 = '31' order by server_date asc"
+        let 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 = '\(MessageScope.GPT_CHATBOT)' order by server_date asc"
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
                 if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {

+ 12 - 12
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -281,7 +281,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         if dataMessageForward != nil {
             for i in 0..<dataMessageForward!.count {
                 let isForwarded = (dataMessageForward![i][TypeDataMessage.is_forwarded] as? Int) ?? 0
-                sendChat(message_scope_id: "4", status: "2", message_text: dataMessageForward![i]["message_text"]  as? String ?? "", credential: "0", attachment_flag: dataMessageForward![i]["attachment_flag"]  as? String ?? "", ex_blog_id: "", message_large_text: "", ex_format: "", image_id: dataMessageForward![i]["image_id"]  as? String ?? "", audio_id: dataMessageForward![i]["audio_id"]  as? String ?? "", video_id: dataMessageForward![i]["video_id"]  as? String ?? "", file_id: dataMessageForward![i]["file_id"]  as? String ?? "", thumb_id: dataMessageForward![i]["thumb_id"]  as? String ?? "", reff_id: "", read_receipts: "", is_call_center: "0", call_center_id: "", viewController: self, gif_id: dataMessageForward![i][TypeDataMessage.gif_id]  as? String ?? "", is_forwarded: isForwarded + 1)
+                sendChat(message_scope_id: MessageScope.GROUP, status: "2", message_text: dataMessageForward![i]["message_text"]  as? String ?? "", credential: "0", attachment_flag: dataMessageForward![i]["attachment_flag"]  as? String ?? "", ex_blog_id: "", message_large_text: "", ex_format: "", image_id: dataMessageForward![i]["image_id"]  as? String ?? "", audio_id: dataMessageForward![i]["audio_id"]  as? String ?? "", video_id: dataMessageForward![i]["video_id"]  as? String ?? "", file_id: dataMessageForward![i]["file_id"]  as? String ?? "", thumb_id: dataMessageForward![i]["thumb_id"]  as? String ?? "", reff_id: "", read_receipts: "", is_call_center: "0", call_center_id: "", viewController: self, gif_id: dataMessageForward![i][TypeDataMessage.gif_id]  as? String ?? "", is_forwarded: isForwarded + 1)
             }
             dataMessageForward = nil
         }
@@ -486,7 +486,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                     if let idx = dataMessages.firstIndex(where: { $0["message_id"] as? String == markerCounter}) {
                         for i in idx..<dataMessages.count {
                             if dataMessages[i]["f_pin"] as? String != idMe {
-                                sendReadMessageStatus(chat_id: self.dataTopic["chat_id"]  as? String ?? "", f_pin: dataMessages[i]["f_pin"]  as? String ?? "", message_scope_id: "4", message_id: dataMessages[i]["message_id"]  as? String ?? "")
+                                sendReadMessageStatus(chat_id: self.dataTopic["chat_id"]  as? String ?? "", f_pin: dataMessages[i]["f_pin"]  as? String ?? "", message_scope_id: MessageScope.GROUP, message_id: dataMessages[i]["message_id"]  as? String ?? "")
                             }
                         }
                         counter = 0
@@ -1228,7 +1228,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
             if let dataMessage = data["message"] as? TMessage {
                 let idMe = User.getMyPin() as String?
                 let chatData = dataMessage.mBodies
-                if (chatData[CoreMessage_TMessageKey.F_PIN] == idMe || chatData[CoreMessage_TMessageKey.L_PIN] == self.dataGroup["group_id"] as? String || chatData[CoreMessage_TMessageKey.F_PIN] == self.dataGroup["group_id"] as? String) && chatData[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID] == "4" {
+                if (chatData[CoreMessage_TMessageKey.F_PIN] == idMe || chatData[CoreMessage_TMessageKey.L_PIN] == self.dataGroup["group_id"] as? String || chatData[CoreMessage_TMessageKey.F_PIN] == self.dataGroup["group_id"] as? String) && chatData[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID] == MessageScope.GROUP {
                     if (chatData.keys.contains(CoreMessage_TMessageKey.MESSAGE_ID) && !(chatData[CoreMessage_TMessageKey.MESSAGE_ID]!).contains("-2,")) {
                         var idx = self.dataMessages.firstIndex(where: { $0["message_id"]  as? String ?? "" == chatData[CoreMessage_TMessageKey.MESSAGE_ID]! })
                         if let idxMessageIdParent = self.groupImages.firstIndex(where: { $0.value.contains(where: { $0.messageId == chatData[CoreMessage_TMessageKey.MESSAGE_ID]! }) }) {
@@ -1619,7 +1619,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                     let idMe = User.getMyPin() as String?
                     for i in 0...listData.count - 1 {
                         if listData[i]["f_pin"] as? String != idMe {
-                            self.sendReadMessageStatus(chat_id: self.dataTopic["chat_id"]  as? String ?? "", f_pin: listData[i]["f_pin"]  as? String ?? "", message_scope_id: "4", message_id: listData[i]["message_id"]  as? String ?? "")
+                            self.sendReadMessageStatus(chat_id: self.dataTopic["chat_id"]  as? String ?? "", f_pin: listData[i]["f_pin"]  as? String ?? "", message_scope_id: MessageScope.GROUP, message_id: listData[i]["message_id"]  as? String ?? "")
                         }
                     }
                 }
@@ -1631,7 +1631,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                     let idMe = User.getMyPin() as String?
                     for i in 0...listData.count - 1 {
                         if listData[i]["f_pin"] as? String != idMe {
-                            self.sendReadMessageStatus(chat_id: self.dataTopic["chat_id"]  as? String ?? "", f_pin: listData[i]["f_pin"]  as? String ?? "", message_scope_id: "4", message_id: listData[i]["message_id"]  as? String ?? "")
+                            self.sendReadMessageStatus(chat_id: self.dataTopic["chat_id"]  as? String ?? "", f_pin: listData[i]["f_pin"]  as? String ?? "", message_scope_id: MessageScope.GROUP, message_id: listData[i]["message_id"]  as? String ?? "")
                         }
                     }
                 }
@@ -1782,7 +1782,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         sendChat(message_text: textFieldSend.text!, viewController: self)
     }
     
-    private func sendChat(message_scope_id:String =  "4", 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 =  "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) {
         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()
@@ -2215,7 +2215,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     
     private func sendTyping(l_pin: String, isTyping: Bool = false) {
         DispatchQueue.global().async {
-            let tmessage = CoreMessage_TMessageBank.getUpdateTypingStatus(p_opposite: l_pin, p_scope: "4", p_status: isTyping ? "3": "4")
+            let tmessage = CoreMessage_TMessageBank.getUpdateTypingStatus(p_opposite: l_pin, p_scope: MessageScope.GROUP, p_status: isTyping ? "3": "4")
             _ = Nexilis.write(message: tmessage)
         }
     }
@@ -2263,7 +2263,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                 let idMe = User.getMyPin() as String?
                 for i in 0...listData.count - 1 {
                     if listData[i]["f_pin"] as? String != idMe {
-                        sendReadMessageStatus(chat_id: self.dataTopic["chat_id"]  as? String ?? "", f_pin: listData[i]["f_pin"]  as? String ?? "", message_scope_id: "4", message_id: listData[i]["message_id"]  as? String ?? "")
+                        sendReadMessageStatus(chat_id: self.dataTopic["chat_id"]  as? String ?? "", f_pin: listData[i]["f_pin"]  as? String ?? "", message_scope_id: MessageScope.GROUP, message_id: listData[i]["message_id"]  as? String ?? "")
                     }
                 }
             }
@@ -4019,7 +4019,7 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
                 if (type == "me") {
                     if let groupingImages = groupImages[dataMessages[i]["message_id"]  as? String ?? ""] {
                         for i in 0..<groupingImages.count {
-                            self.deleteMessage(l_pin: dataGroup["group_id"]  as? String ?? "", message_id: groupingImages[i].messageId, scope: "4", type: "1", chat: dataTopic["chat_id"]  as? String ?? "")
+                            self.deleteMessage(l_pin: dataGroup["group_id"]  as? String ?? "", message_id: groupingImages[i].messageId, scope: MessageScope.GROUP, type: "1", chat: dataTopic["chat_id"]  as? String ?? "")
                             let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == groupingImages[i].messageId })
                             if idx != nil {
                                 self.dataMessages.remove(at: idx!)
@@ -4043,7 +4043,7 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
                         } else {
                             if let groupingImages = groupImages[dataMessages[i]["message_id"]  as? String ?? ""] {
                                 for i in 0..<groupingImages.count {
-                                    self.deleteMessage(l_pin: dataGroup["group_id"]  as? String ?? "", message_id: groupingImages[i].messageId, scope: "4", type: "2", chat: dataTopic["chat_id"]  as? String ?? "")
+                                    self.deleteMessage(l_pin: dataGroup["group_id"]  as? String ?? "", message_id: groupingImages[i].messageId, scope: MessageScope.GROUP, type: "2", chat: dataTopic["chat_id"]  as? String ?? "")
                                     let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == groupingImages[i].messageId})
                                     if idx != nil {
                                         self.dataMessages[idx!]["lock"] = "1"
@@ -4058,7 +4058,7 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
                                     self.groupImages.removeValue(forKey: groupingImages[0].messageId)
                                 }
                             } else {
-                                self.deleteMessage(l_pin: dataGroup["group_id"]  as? String ?? "", message_id: dataMessages[i]["message_id"]  as? String ?? "", scope: "4", type: "1", chat: dataTopic["chat_id"]  as? String ?? "")
+                                self.deleteMessage(l_pin: dataGroup["group_id"]  as? String ?? "", message_id: dataMessages[i]["message_id"]  as? String ?? "", scope: MessageScope.GROUP, type: "1", chat: dataTopic["chat_id"]  as? String ?? "")
                                 let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == dataMessages[i]["message_id"] as? String})
                                 if idx != nil {
                                     self.dataMessages.remove(at: idx!)
@@ -4075,7 +4075,7 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
                         }
                     }
                 } else {
-                    self.deleteMessage(l_pin: dataGroup["group_id"]  as? String ?? "", message_id: dataMessages[i]["message_id"]  as? String ?? "", scope: "4", type: "2", chat: dataTopic["chat_id"]  as? String ?? "")
+                    self.deleteMessage(l_pin: dataGroup["group_id"]  as? String ?? "", message_id: dataMessages[i]["message_id"]  as? String ?? "", scope: MessageScope.GROUP, type: "2", chat: dataTopic["chat_id"]  as? String ?? "")
                     let idx = self.dataMessages.firstIndex(where: { $0["message_id"]  as? String ?? "" == dataMessages[i]["message_id"]  as? String ?? ""})
                     if idx != nil {
                         self.dataMessages[idx!]["lock"] = "1"

+ 159 - 26
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -265,6 +265,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         center.addObserver(self, selector: #selector(onUnfriend(notification:)), name: NSNotification.Name(rawValue: "onUpdatePersonInfo"), object: nil)
         center.addObserver(self, selector: #selector(onTyping(notification:)), name: NSNotification.Name(rawValue: Nexilis.listenerTypingChat), object: nil)
         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)
         
         locationManager.delegate = self
         locationManager.requestWhenInUseAuthorization()
@@ -282,7 +283,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         if dataMessageForward != nil {
             for i in 0..<dataMessageForward!.count {
                 let isForwarded = (dataMessageForward![i][TypeDataMessage.is_forwarded] as? Int) ?? 0
-                sendChat(message_scope_id: "3", status: "2", message_text: dataMessageForward![i]["message_text"]  as? String ?? "", credential: "0", attachment_flag: dataMessageForward![i]["attachment_flag"]  as? String ?? "", ex_blog_id: "", message_large_text: "", ex_format: "", image_id: dataMessageForward![i]["image_id"]  as? String ?? "", audio_id: dataMessageForward![i]["audio_id"]  as? String ?? "", video_id: dataMessageForward![i]["video_id"]  as? String ?? "", file_id: dataMessageForward![i]["file_id"]  as? String ?? "", thumb_id: dataMessageForward![i]["thumb_id"]  as? String ?? "", reff_id: "", read_receipts: dataMessageForward![i]["read_receipts"]  as? String ?? "", chat_id: "", is_call_center: "0", call_center_id: "", viewController: self, gif_id: dataMessageForward![i][TypeDataMessage.gif_id]  as? String ?? "", is_forwarded: isForwarded + 1, is_secret: (dataMessageForward![i][TypeDataMessage.is_secret] as? Int) ?? 0)
+                sendChat(message_scope_id: MessageScope.WHISPER, status: "2", message_text: dataMessageForward![i]["message_text"]  as? String ?? "", credential: "0", attachment_flag: dataMessageForward![i]["attachment_flag"]  as? String ?? "", ex_blog_id: "", message_large_text: "", ex_format: "", image_id: dataMessageForward![i]["image_id"]  as? String ?? "", audio_id: dataMessageForward![i]["audio_id"]  as? String ?? "", video_id: dataMessageForward![i]["video_id"]  as? String ?? "", file_id: dataMessageForward![i]["file_id"]  as? String ?? "", thumb_id: dataMessageForward![i]["thumb_id"]  as? String ?? "", reff_id: "", read_receipts: dataMessageForward![i]["read_receipts"]  as? String ?? "", chat_id: "", is_call_center: "0", call_center_id: "", viewController: self, gif_id: dataMessageForward![i][TypeDataMessage.gif_id]  as? String ?? "", is_forwarded: isForwarded + 1, is_secret: (dataMessageForward![i][TypeDataMessage.is_secret] as? Int) ?? 0)
             }
             dataMessageForward = nil
         }
@@ -363,7 +364,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                 alert.addAction(UIAlertAction(title: "Delete".localized(), style: .destructive, handler: {(_) in
                     Database.shared.database?.inTransaction({ (fmdb, rollback) in
                         do {
-                            _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE", _where: "(f_pin='\(self.dataPerson["f_pin"]!!)' or l_pin='\(self.dataPerson["f_pin"]!!)') and (message_scope_id='3' or message_scope_id='18') and is_call_center = 0")
+                            _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE", _where: "(f_pin='\(self.dataPerson["f_pin"]!!)' or l_pin='\(self.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")
                             _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", _where: "l_pin='\(self.dataPerson["f_pin"]!!)'")
                             let l_pin = self.dataPerson["f_pin"]!!
                             SecureUserDefaults.shared.removeValue(forKey: "new_saved_\(l_pin)")
@@ -621,7 +622,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                         if let idx = dataMessages.firstIndex(where: { $0["message_id"] as? String == markerCounter}) {
                             for i in idx..<dataMessages.count {
                                 if dataMessages[i]["f_pin"] as? String != idMe {
-                                    sendReadMessageStatus(chat_id: "", f_pin: dataPerson["f_pin"]!!, message_scope_id: "3", message_id: dataMessages[i]["message_id"]  as? String ?? "")
+                                    sendReadMessageStatus(chat_id: "", f_pin: dataPerson["f_pin"]!!, message_scope_id: MessageScope.WHISPER, message_id: dataMessages[i]["message_id"]  as? String ?? "")
                                 }
                             }
                             counter = 0
@@ -653,7 +654,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             let idMe = User.getMyPin() as String?
             for i in 0..<dataMessages.count {
                 if dataMessages[i]["f_pin"] as? String != idMe {
-                    sendReadMessageStatus(chat_id: "", f_pin: dataPerson["f_pin"]!!, message_scope_id: "3", message_id: dataMessages[i]["message_id"]  as? String ?? "")
+                    sendReadMessageStatus(chat_id: "", f_pin: dataPerson["f_pin"]!!, message_scope_id: MessageScope.WHISPER, message_id: dataMessages[i]["message_id"]  as? String ?? "")
                 }
             }
             tableChatView.scrollToBottom(isAnimated: false)
@@ -936,8 +937,8 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     
     private func addDataMessage() {
         multipleOffsetUp += 1
-        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"
-        let 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 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*\(multipleOffsetUp-1) ELSE (\(queryCount))-\(dataMessages.count) END OFFSET CASE WHEN (\(queryCount))>=\(20*multipleOffsetUp) THEN (\(queryCount))-\(20*multipleOffsetUp) ELSE 0 END"
+        let queryCount = "SELECT COUNT(*) 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"
+        let 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 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 CASE WHEN (\(queryCount))-\(dataMessages.count)>=20 THEN 20*\(multipleOffsetUp-1) ELSE (\(queryCount))-\(dataMessages.count) END OFFSET CASE WHEN (\(queryCount))>=\(20*multipleOffsetUp) THEN (\(queryCount))-\(20*multipleOffsetUp) ELSE 0 END"
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
                 if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
@@ -1081,12 +1082,12 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     private func getData() {
 //        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 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"
+        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 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 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 = '5' 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"
             } 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 = '5' 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"
             }
             if isRequestContactCenter && !isDirectCC {
                 viewButton.isHidden = true
@@ -1592,7 +1593,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                         self.changeAppBar()
                     }
                 }
-                else if (chatData[CoreMessage_TMessageKey.F_PIN] == self.dataPerson["f_pin"]!! && !self.isContactCenter && (chatData[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID] == "3")) || (self.isContactCenter && chatData[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID] == "5" && self.complaintId == chatData[CoreMessage_TMessageKey.CALL_CENTER_ID]) {
+                else if (chatData[CoreMessage_TMessageKey.F_PIN] == self.dataPerson["f_pin"]!! && !self.isContactCenter && (chatData[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID] == MessageScope.WHISPER)) || (self.isContactCenter && chatData[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID] == "5" && self.complaintId == chatData[CoreMessage_TMessageKey.CALL_CENTER_ID]) {
                     if chatData[CoreMessage_TMessageKey.F_PIN] == nil {
                         return
                     }
@@ -1906,6 +1907,58 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         }
     }
     
+    @objc func onRefreshCallLog(notification: NSNotification) {
+        DispatchQueue.main.async {
+            let data:[AnyHashable : Any] = notification.userInfo!
+            let messageId = data["message_id"]  as? String ?? ""
+            self.appendNewMessage(messageId: messageId)
+        }
+    }
+    
+    private func appendNewMessage(messageId: String) {
+        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+            if let cursorData = Database.shared.getRecords(fmdb: fmdb, 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 from MESSAGE where message_id = '\(messageId)'"), cursorData.next() {
+                var row: [String: Any?] = [:]
+                row["message_id"] = cursorData.string(forColumnIndex: 0)
+                row["f_pin"] = cursorData.string(forColumnIndex: 1)
+                row["l_pin"] = cursorData.string(forColumnIndex: 2)
+                row["message_scope_id"] = cursorData.string(forColumnIndex: 3)
+                row["server_date"] = cursorData.string(forColumnIndex: 4)
+                row["status"] = cursorData.string(forColumnIndex: 5)
+                row["message_text"] = cursorData.string(forColumnIndex: 6)
+                row["audio_id"] = cursorData.string(forColumnIndex: 7)
+                row["video_id"] = cursorData.string(forColumnIndex: 8)
+                row["image_id"] = cursorData.string(forColumnIndex: 9)
+                row["thumb_id"] = cursorData.string(forColumnIndex: 10)
+                row["read_receipts"] = cursorData.string(forColumnIndex: 11)
+                row["chat_id"] = cursorData.string(forColumnIndex: 12)
+                row["file_id"] = cursorData.string(forColumnIndex: 13)
+                row["attachment_flag"] = cursorData.string(forColumnIndex: 14)
+                row["reff_id"] = cursorData.string(forColumnIndex: 15)
+                row["lock"] = cursorData.string(forColumnIndex: 16)
+                row["is_stared"] = cursorData.string(forColumnIndex: 17)
+                row["blog_id"] = cursorData.string(forColumnIndex: 18)
+                row["credential"] = cursorData.string(forColumnIndex: 19)
+                row[TypeDataMessage.is_call_center] = cursorData.string(forColumnIndex: 20)
+                row[TypeDataMessage.call_center_id] = cursorData.string(forColumnIndex: 21)
+                row[TypeDataMessage.opposite_pin] = cursorData.string(forColumnIndex: 22)
+                row[TypeDataMessage.last_edit] = cursorData.longLongInt(forColumnIndex: 23)
+                row[TypeDataMessage.gif_id] = cursorData.string(forColumnIndex: 24)
+                row[TypeDataMessage.is_forwarded] = Int(cursorData.int(forColumnIndex: 25))
+                row["progress"] = 0.0
+                row["isSelected"] = false
+                if !self.dataDates.contains("Today".localized()) {
+                    self.dataDates.append("Today".localized())
+                    self.tableChatView.insertSections(IndexSet(integer: self.dataDates.count - 1), with: .none)
+                }
+                row["chat_date"] = "Today".localized()
+                dataMessages.append(row)
+                self.tableChatView.insertRows(at: [IndexPath(row: self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == self.dataDates[self.dataDates.count - 1]}).count - 1, section: self.dataDates.count - 1)], with: .none)
+                cursorData.close()
+            }
+        })
+    }
+    
     private func updateStatusDelete(idx: Int?, chatData: [String: String]) {
         do {
             if self.dataMessages[idx!]["lock"] != nil && self.dataMessages[idx!]["lock"]  as? String ?? "" == "1" {
@@ -2078,7 +2131,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                     let idMe = User.getMyPin() as String?
                     for i in 0...listData.count - 1 {
                         if listData[i]["f_pin"] as? String != idMe {
-                            self.sendReadMessageStatus(chat_id: "", f_pin: self.dataPerson["f_pin"]!!, message_scope_id: "3", message_id: listData[i]["message_id"]  as? String ?? "")
+                            self.sendReadMessageStatus(chat_id: "", f_pin: self.dataPerson["f_pin"]!!, message_scope_id: MessageScope.WHISPER, message_id: listData[i]["message_id"]  as? String ?? "")
                         }
                     }
                 }
@@ -2090,7 +2143,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                     let idMe = User.getMyPin() as String?
                     for i in 0...listData.count - 1 {
                         if listData[i]["f_pin"] as? String != idMe {
-                            self.sendReadMessageStatus(chat_id: "", f_pin: self.dataPerson["f_pin"]!!, message_scope_id: "3", message_id: listData[i]["message_id"]  as? String ?? "")
+                            self.sendReadMessageStatus(chat_id: "", f_pin: self.dataPerson["f_pin"]!!, message_scope_id: MessageScope.WHISPER, message_id: listData[i]["message_id"]  as? String ?? "")
                         }
                     }
                 }
@@ -2570,7 +2623,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         }
     }
     
-    private func sendChat(message_scope_id:String =  "3", 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 = "4", chat_id: String = "", is_call_center: String = "0", call_center_id: String = "", viewController: UIViewController, isAutoSendCC : Bool = false, gif_id: String = "", is_forwarded: Int = 0, is_secret: Int = 0) {
+    private func sendChat(message_scope_id:String =  MessageScope.WHISPER, 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 = "4", chat_id: String = "", is_call_center: String = "0", call_center_id: String = "", viewController: UIViewController, isAutoSendCC : Bool = false, gif_id: String = "", is_forwarded: Int = 0, is_secret: Int = 0) {
         if viewController is EditorPersonal && file_id == "" && dataMessageForward == nil && !isAutoSendCC{
             if ((textFieldSend.text!.trimmingCharacters(in: .whitespacesAndNewlines) == "Send message".localized() && textFieldSend.textColor == UIColor.lightGray && attachment_flag != "11") || textFieldSend.text!.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ) {
                 dismissKeyboard()
@@ -2609,7 +2662,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             is_call_center = "1"
             call_center_id = complaintId
             l_pin = fPinContacCenter
-            message_scope_id = "5"
+            message_scope_id = MessageScope.CHATROOM
             chat_id = complaintId
             if isAutoSendCC {
                 timeoutCC = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: false, block: {_ in
@@ -2779,7 +2832,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         DispatchQueue.global().async {
             if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getAddFriendApproval(lPin: lPin ?? "", isAccept: isAccept), timeout: 5 * 1000) {
                 if response.isOk() {
-                    self.deleteMessage(l_pin: User.getMyPin() ?? "", message_id: messageId ?? "", scope: "3", type: "1", chat: "")
+                    self.deleteMessage(l_pin: User.getMyPin() ?? "", message_id: messageId ?? "", scope: MessageScope.WHISPER, type: "1", chat: "")
                     let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == messageId})
                     if idx != nil {
                         self.dataMessages.remove(at: idx!)
@@ -3385,7 +3438,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     
     private func sendTyping(l_pin: String, isTyping: Bool = false) {
         DispatchQueue.global().async {
-            let tmessage = CoreMessage_TMessageBank.getUpdateTypingStatus(p_opposite: l_pin, p_scope: "3", p_status: isTyping ? "3": "4")
+            let tmessage = CoreMessage_TMessageBank.getUpdateTypingStatus(p_opposite: l_pin, p_scope: MessageScope.WHISPER, p_status: isTyping ? "3": "4")
             _ = Nexilis.write(message: tmessage)
         }
     }
@@ -3534,7 +3587,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                 let idMe = User.getMyPin() as String?
                 for i in 0...listData.count - 1 {
                     if listData[i]["f_pin"] as? String != idMe {
-                        sendReadMessageStatus(chat_id: "", f_pin: dataPerson["f_pin"]!!, message_scope_id: "3", message_id: listData[i]["message_id"]  as? String ?? "")
+                        sendReadMessageStatus(chat_id: "", f_pin: dataPerson["f_pin"]!!, message_scope_id: MessageScope.WHISPER, message_id: listData[i]["message_id"]  as? String ?? "")
                     }
                 }
             }
@@ -4965,7 +5018,7 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
             contactChatNav.view.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
             if let controller = contactChatNav.viewControllers.first as? ContactChatViewController {
                 controller.isChooser = { [weak self] scope, pin in
-                    if scope == "3" {
+                    if scope == MessageScope.WHISPER {
                         let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
                         editorPersonalVC.unique_l_pin = pin
                         editorPersonalVC.dataMessageForward = dataMessages
@@ -5144,7 +5197,7 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
                 if (type == "me") {
                     if let groupingImages = groupImages[dataMessages[i]["message_id"]  as? String ?? ""] {
                         for i in 0..<groupingImages.count {
-                            self.deleteMessage(l_pin: groupingImages[i].lPin, message_id: groupingImages[i].messageId, scope: "3", type: "1", chat: "")
+                            self.deleteMessage(l_pin: groupingImages[i].lPin, message_id: groupingImages[i].messageId, scope: MessageScope.WHISPER, type: "1", chat: "")
                             let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == groupingImages[i].messageId })
                             if idx != nil {
                                 self.dataMessages.remove(at: idx!)
@@ -5160,7 +5213,7 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
                         }
                         self.groupImages.removeValue(forKey: groupingImages[0].messageId)
                     } else {
-                        self.deleteMessage(l_pin: dataMessages[i]["l_pin"]  as? String ?? "", message_id: dataMessages[i]["message_id"]  as? String ?? "", scope: "3", type: "1", chat: "")
+                        self.deleteMessage(l_pin: dataMessages[i]["l_pin"]  as? String ?? "", message_id: dataMessages[i]["message_id"]  as? String ?? "", scope: MessageScope.WHISPER, type: "1", chat: "")
                         let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == dataMessages[i]["message_id"] as? String})
                         if idx != nil {
                             self.dataMessages.remove(at: idx!)
@@ -5183,7 +5236,7 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
                     } else {
                         if let groupingImages = groupImages[dataMessages[i]["message_id"]  as? String ?? ""] {
                             for i in 0..<groupingImages.count {
-                                self.deleteMessage(l_pin: groupingImages[i].lPin, message_id: groupingImages[i].messageId, scope: "3", type: "2", chat: "")
+                                self.deleteMessage(l_pin: groupingImages[i].lPin, message_id: groupingImages[i].messageId, scope: MessageScope.WHISPER, type: "2", chat: "")
                                 let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == groupingImages[i].messageId})
                                 if idx != nil {
                                     self.dataMessages[idx!]["lock"] = "1"
@@ -5198,7 +5251,7 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
                                 self.groupImages.removeValue(forKey: groupingImages[0].messageId)
                             }
                         } else {
-                            self.deleteMessage(l_pin: dataMessages[i]["l_pin"]  as? String ?? "", message_id: dataMessages[i]["message_id"]  as? String ?? "", scope: "3", type: "2", chat: "")
+                            self.deleteMessage(l_pin: dataMessages[i]["l_pin"]  as? String ?? "", message_id: dataMessages[i]["message_id"]  as? String ?? "", scope: MessageScope.WHISPER, type: "2", chat: "")
                             let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == dataMessages[i]["message_id"] as? String})
                             if idx != nil {
                                 self.dataMessages[idx!]["lock"] = "1"
@@ -5846,7 +5899,7 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
             
             timeMessage.trailingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: -8).isActive = true
             
-            if (dataMessages[indexPath.row]["lock"] as? String == "0" || (dataMessages[indexPath.row]["lock"] as? String ?? "").isEmpty) {
+            if (dataMessages[indexPath.row]["lock"] as? String == "0" || (dataMessages[indexPath.row]["lock"] as? String ?? "").isEmpty)  && dataMessages[indexPath.row][TypeDataMessage.message_scope_id] as? String != MessageScope.CALL && dataMessages[indexPath.row][TypeDataMessage.message_scope_id] as? String != MessageScope.MISSED_CALL {
                 cell.contentView.addSubview(statusMessage)
                 statusMessage.translatesAutoresizingMaskIntoConstraints = false
                 statusMessage.bottomAnchor.constraint(equalTo: timeMessage.topAnchor).isActive = true
@@ -6256,13 +6309,91 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
             }
         }
         
+        if dataMessages[indexPath.row][TypeDataMessage.message_scope_id] as? String == MessageScope.CALL || dataMessages[indexPath.row][TypeDataMessage.message_scope_id] as? String == MessageScope.MISSED_CALL{
+            messageText.removeFromSuperview()
+            
+            let containerCall = UIButton(type: .custom)
+            containerCall.backgroundColor = .white.withAlphaComponent(0.3)
+            containerMessage.addSubview(containerCall)
+            containerCall.anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, right: containerMessage.rightAnchor, paddingTop: 5, paddingLeft: 5, paddingBottom: 5, paddingRight: 5, height: 60)
+            containerCall.layer.cornerRadius = 5
+            containerCall.clipsToBounds = true
+            
+            var imageCall = "phone.fill.arrow.up.right"
+            var textCall = "Audio call".localized()
+            let isVideo = textChat.lowercased().contains("video")
+            let isMissedCall = textChat.lowercased().contains("missed")
+            let isImageLeft = textChat.lowercased().contains("incoming") || isMissedCall
+            let longCall = textChat.components(separatedBy: " at ")[1]
+            var subTextCall = longCall
+            
+            let contIconCall = UIView(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
+            containerCall.addSubview(contIconCall)
+            contIconCall.anchor(top: containerCall.topAnchor, left: containerCall.leftAnchor, bottom: containerCall.bottomAnchor, paddingTop: 10, paddingLeft: 10, paddingBottom: 10, width: 40, height: 40)
+            contIconCall.circle()
+            if isImageLeft {
+                contIconCall.backgroundColor = .white
+            } else {
+                contIconCall.backgroundColor = .black.withAlphaComponent(0.6)
+            }
+            
+            if isVideo && isImageLeft {
+                imageCall = "arrow.down.left.video.fill"
+                if isMissedCall {
+                    textCall = "Missed video call".localized()
+                    subTextCall = "Tap to call back".localized()
+                } else {
+                    textCall = "Video call".localized()
+                }
+            } else if isVideo {
+                imageCall = "arrow.up.right.video.fill"
+                textCall = "Video call".localized()
+                if longCall.trimmingCharacters(in: .whitespaces) == "0" {
+                    subTextCall = "No answer".localized()
+                }
+            } else if isImageLeft {
+                imageCall = "phone.fill.arrow.down.left"
+                if isMissedCall {
+                    textCall = "Missed audio call".localized()
+                    subTextCall = "Tap to call back".localized()
+                }
+            } else if longCall.trimmingCharacters(in: .whitespaces) == "0" {
+                subTextCall = "No answer".localized()
+            }
+            
+            let iconCall = UIImageView()
+            iconCall.image = UIImage(systemName: imageCall, withConfiguration: UIImage.SymbolConfiguration(pointSize: 18))
+            contIconCall.addSubview(iconCall)
+            if isMissedCall {
+                iconCall.tintColor = .red
+            }else if isImageLeft {
+                iconCall.tintColor = .black
+            } else {
+                iconCall.tintColor = .white
+            }
+            iconCall.anchor(centerX: contIconCall.centerXAnchor, centerY: contIconCall.centerYAnchor)
+            
+            let titleCall = UILabel()
+            containerCall.addSubview(titleCall)
+            titleCall.anchor(top: containerCall.topAnchor, left: contIconCall.rightAnchor, right: containerCall.rightAnchor, paddingTop: 10, paddingLeft: 10, paddingRight: 10)
+            titleCall.text = textCall
+            titleCall.font = .systemFont(ofSize: 14)
+            
+            let subtitleCall = UILabel()
+            containerCall.addSubview(subtitleCall)
+            subtitleCall.anchor(top: titleCall.bottomAnchor, left: contIconCall.rightAnchor, right: containerCall.rightAnchor, paddingLeft: 10, paddingRight: 10)
+            subtitleCall.text = subTextCall
+            subtitleCall.font = .systemFont(ofSize: 13)
+            subtitleCall.textColor = .gray
+        }
+        
         if !copySession && !forwardSession && !deleteSession && !self.removed {
             let interaction = UIContextMenuInteraction(delegate: self)
             containerMessage.addInteraction(interaction)
             containerMessage.isUserInteractionEnabled = true
         }
         
-        if isSearching && textSearch.count > 1 {
+        if isSearching && textSearch.count > 1 && dataMessages[indexPath.row][TypeDataMessage.message_scope_id] as? String != MessageScope.CALL && dataMessages[indexPath.row][TypeDataMessage.message_scope_id] as? String != MessageScope.MISSED_CALL {
             messageText.attributedText = messageRequestFriend != nil ? messageRequestFriend.richText(isSearching: true, textSearch: textSearch) : stringLS.isEmpty ? textChat.richText(isSearching: true, textSearch: textSearch) : stringLS.richText(isSearching: true, textSearch: textSearch)
         }
         
@@ -6457,7 +6588,7 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                     timeInImage.textColor = .white
                     timeInImage.font = UIFont.systemFont(ofSize: 10 + offset(), weight: .medium)
                     
-                    if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+                    if (dataMessages[indexPath.row]["f_pin"] as? String == idMe && dataMessages[indexPath.row][TypeDataMessage.message_scope_id] as? String != MessageScope.CALL && dataMessages[indexPath.row][TypeDataMessage.message_scope_id] as? String != MessageScope.MISSED_CALL) {
                         let statusInImage = UIImageView()
                         containerTimeStatus.addSubview(statusInImage)
                         statusInImage.anchor(right: containerTimeStatus.rightAnchor, centerY: containerTimeStatus.centerYAnchor, width: 15, height: 15)
@@ -7159,7 +7290,9 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
             let textForwarded = "Forwarded".localized()
             titleForwarded.attributedText = " $\(textForwarded)$".richText()
         }
-        topMarginText.isActive = true
+        if messageText.isDescendant(of: containerMessage) {
+            topMarginText.isActive = true
+        }
 //        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureCellAction))
 //        panGestureRecognizer.delegate = self
 //        cellMessage.addGestureRecognizer(panGestureRecognizer)

+ 10 - 0
NexilisLite/NexilisLite/Source/View/Contact/ContactCallViewController.swift

@@ -112,6 +112,8 @@ class ContactCallViewController: UIViewController {
         
         let center: NotificationCenter = NotificationCenter.default
         center.addObserver(self, selector: #selector(refresh(notification:)), name: NSNotification.Name(rawValue: "onUpdatePersonInfo"), object: nil)
+        
+        pullBuddy()
     }
     
     @objc func addFriend(sender: UIBarButtonItem) {
@@ -204,6 +206,14 @@ class ContactCallViewController: UIViewController {
         fillteredData = self.dataPerson.filter { $0["name"]!!.lowercased().contains(searchText.lowercased()) }
         tableView.reloadData()
     }
+    
+    private func pullBuddy() {
+        if let me = User.getMyPin() {
+            DispatchQueue.global().async {
+                let _ = Nexilis.write(message: CoreMessage_TMessageBank.getBatchBuddiesInfos(p_f_pin: me, last_update: 0))
+            }
+        }
+    }
 }
 
 extension ContactCallViewController: UITableViewDelegate {

+ 5 - 5
NexilisLite/NexilisLite/Source/View/Control/ContactChatViewController.swift

@@ -702,7 +702,7 @@ extension ContactChatViewController {
                     smartChatVC.hidesBottomBarWhenPushed = true
                     smartChatVC.fromNotification = false
                     navigationController?.show(smartChatVC, sender: nil)
-                } else if data.messageScope == "3" {
+                } else if data.messageScope == MessageScope.WHISPER || data.messageScope == MessageScope.CALL || data.messageScope == MessageScope.MISSED_CALL {
                     let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
                     editorPersonalVC.hidesBottomBarWhenPushed = true
                     editorPersonalVC.unique_l_pin = data.pin
@@ -1176,7 +1176,7 @@ extension ContactChatViewController {
                 ])
                 var leadingAnchor = imageView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 10.0)
                 if data.profile.isEmpty && data.pin != "-999" && data.pin != "-997" {
-                    if data.messageScope == "3" {
+                    if data.messageScope == MessageScope.WHISPER || data.messageScope == MessageScope.CALL || data.messageScope == MessageScope.MISSED_CALL {
                         imageView.image = UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
                     } else {
                         imageView.image = UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
@@ -1224,8 +1224,8 @@ extension ContactChatViewController {
                             }
                         }
                     } else {
-                        if data.messageScope == "3" || data.isParent || data.pin == "-999" {
-                            getImage(name: data.profile, placeholderImage: UIImage(named: data.pin == "-999" ? "pb_button" : data.messageScope == "3" ? "Profile---Purple" : "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
+                        if data.messageScope == MessageScope.WHISPER || data.messageScope == MessageScope.CALL || data.messageScope == MessageScope.MISSED_CALL || data.isParent || data.pin == "-999" {
+                            getImage(name: data.profile, placeholderImage: UIImage(named: data.pin == "-999" ? "pb_button" : (data.messageScope == MessageScope.WHISPER || data.messageScope == MessageScope.CALL || data.messageScope == MessageScope.MISSED_CALL) ? "Profile---Purple" : "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
                                 imageView.image = image
                             })
                         } else {
@@ -1351,7 +1351,7 @@ extension ContactChatViewController {
                                     stringMessage.append(NSAttributedString(string: "You".localized() + ": ", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12 + String.offset(), weight: .medium)]))
                                 }
                                 stringMessage.append(("🚫 _"+"You were deleted this message".localized()+"_").richText())
-                            } else {
+                            } else if data.messageScope != MessageScope.CALL && data.messageScope != MessageScope.MISSED_CALL {
                                 let imageStatus = NSTextAttachment()
                                 let status = getRealStatus(messageId: data.messageId)
                                 if status == "0" {

+ 1 - 1
NexilisLite/NexilisLite/Source/View/Control/HistoryBroadcastViewController.swift

@@ -256,7 +256,7 @@ class HistoryBroadcastViewController: UIViewController, UITableViewDelegate, UIT
         DispatchQueue.global().async {
             self.chats.removeAll()
             self.chats.append(contentsOf: Chat.getData())
-            self.chats = self.chats.filter({($0.official == "1" || $0.pin == "-999") && $0.messageScope == "3"})
+            self.chats = self.chats.filter({($0.official == "1" || $0.pin == "-999") && $0.messageScope == MessageScope.WHISPER})
             completion()
         }
     }