Explorar el Código

update and fix bugs for release 5.0.42

alqindiirsyam hace 2 meses
padre
commit
2420cc03cc
Se han modificado 24 ficheros con 1255 adiciones y 190 borrados
  1. 4 4
      AppBuilder/AppBuilder.xcodeproj/project.pbxproj
  2. 2 5
      AppBuilder/AppBuilder/FourthTabViewController.swift
  3. 2 0
      AppBuilder/AppBuilder/Info.plist
  4. 232 7
      AppBuilder/AppBuilder/SecondTabViewController.swift
  5. 1 0
      AppBuilder/AppBuilder/ViewController.swift
  6. BIN
      NexilisLite/.DS_Store
  7. 9 10
      NexilisLite/NexilisLite/Source/APIS.swift
  8. 38 0
      NexilisLite/NexilisLite/Source/Database.swift
  9. 142 76
      NexilisLite/NexilisLite/Source/Extension.swift
  10. 1 2
      NexilisLite/NexilisLite/Source/IncomingThread.swift
  11. 17 8
      NexilisLite/NexilisLite/Source/Model/Chat.swift
  12. 9 9
      NexilisLite/NexilisLite/Source/Model/Group.swift
  13. 14 9
      NexilisLite/NexilisLite/Source/Nexilis.swift
  14. 6 4
      NexilisLite/NexilisLite/Source/Utils.swift
  15. 489 0
      NexilisLite/NexilisLite/Source/View/Chat/ArchivedChatView.swift
  16. 4 3
      NexilisLite/NexilisLite/Source/View/Chat/ChatWALikeVC.swift
  17. 3 3
      NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift
  18. 12 6
      NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift
  19. 3 6
      NexilisLite/NexilisLite/Source/View/Control/ChangeDeviceViewController.swift
  20. 1 2
      NexilisLite/NexilisLite/Source/View/Control/ConfigureFloatingButton.swift
  21. 259 26
      NexilisLite/NexilisLite/Source/View/Control/ContactChatViewController.swift
  22. 3 2
      NexilisLite/NexilisLite/Source/View/Control/GroupDetailViewController.swift
  23. 1 2
      NexilisLite/NexilisLite/Source/View/Control/SettingTableViewController.swift
  24. 3 6
      NexilisLite/NexilisLite/Source/View/Control/SignUpSignIn.swift

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

@@ -560,7 +560,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.39;
+				MARKETING_VERSION = 5.0.42;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -596,7 +596,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.39;
+				MARKETING_VERSION = 5.0.42;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -632,7 +632,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.39;
+				MARKETING_VERSION = 5.0.42;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -671,7 +671,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.39;
+				MARKETING_VERSION = 5.0.42;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 2 - 5
AppBuilder/AppBuilder/FourthTabViewController.swift

@@ -833,13 +833,10 @@ public class FourthTabViewController: UIViewController, UITableViewDelegate, UIT
                                     imageView.tintColor = .white
                                     let banner = FloatingNotificationBanner(title: "Successfully Sign-Out".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
                                     banner.show()
-                                    if viewController is UINavigationController {
-                                        viewController = (viewController as! UINavigationController).viewControllers[0]
-                                    }
-                                    if Nexilis.showFB && viewController is ViewController{
+                                    if Nexilis.showFB {
                                         Nexilis.floatingButton.removeFromSuperview()
                                         Nexilis.floatingButton = FloatingButton()
-                                        Nexilis.addFB(viewController: viewController!, fromMAB: true)
+                                        Nexilis.addFB()
                                     }
                                     var dataImage: [AnyHashable : Any] = [:]
                                     dataImage["name"] = ""

+ 2 - 0
AppBuilder/AppBuilder/Info.plist

@@ -101,6 +101,8 @@
     <string>false</string>
     <key>NSFileProtectionComplete</key>
     <true/>
+    <key>NSFaceIDUsageDescription</key>
+    <string>$(PRODUCT_NAME) Authentication with TouchId or FaceID for Security App</string>
 	<key>CFBundleInfoDictionaryVersion</key>
 	<string>6.0</string>
 	<key>CFBundleName</key>

+ 232 - 7
AppBuilder/AppBuilder/SecondTabViewController.swift

@@ -20,6 +20,9 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
     var chats: [Chat] = []
     var chatGroupMaps: [String: [Chat]] = [:]
     
+    var archivedChats: [Chat] = []
+    var listMaxArchived: [String: [String]] = [:]
+    
     var groups: [Group] = []
     
     var cancelSearchButton = UIBarButtonItem()
@@ -769,7 +772,7 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
     }
     
     private func reloadAllData() {
-        print("reloadAllData")
+//        print("reloadAllData")
         DispatchQueue.main.async { [weak self] in
             if self?.timerReloadData == nil {
                 self?.getData()
@@ -860,7 +863,6 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
         if self.isGettingData {
             return
         }
-        self.isGettingData = true
         getChats {
             self.getGroups { g1 in
                 self.groupMap.removeAll()
@@ -885,14 +887,24 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
     
     func getChats(completion: @escaping ()->()) {
         DispatchQueue.global().async {
+//            while self.isGettingData {
+//                Thread.sleep(forTimeInterval: 0.1)
+//            }
+            self.isGettingData = true
             self.chatGroupMaps.removeAll()
+            self.listMaxArchived.removeAll()
             let previousChat = self.chats
             let allChats = Chat.getData()
+            self.archivedChats = Chat.getData(isArchived: true)
             var tempChats: [Chat] = []
+            var lowestPinned = 0
 
             for singleChat in allChats {
                 guard !singleChat.groupId.isEmpty else {
                     tempChats.append(singleChat)
+                    if singleChat.pinned > 0 {
+                        self.listMaxArchived[singleChat.pin] = [""]
+                    }
                     continue
                 }
                 
@@ -906,6 +918,31 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
                         if let counterParent = Int(tempChats[parentChatIndex].counter), let counterSingle = Int(singleChat.counter) {
                             tempChats[parentChatIndex].counter = "\(counterParent + counterSingle)"
                         }
+                        if singleChat.pinned != 0 && (lowestPinned == 0 || lowestPinned > singleChat.pinned) {
+                            lowestPinned = singleChat.pinned
+                        }
+                        if singleChat.pinned != 0 {
+                            if !self.listMaxArchived.keys.contains(singleChat.groupId) {
+                                self.listMaxArchived[singleChat.groupId] = [singleChat.pin]
+                            } else {
+                                self.listMaxArchived[singleChat.groupId]?.append(singleChat.pin)
+                            }
+                        }
+                        if tempChats[parentChatIndex].pinned < singleChat.pinned {
+                            tempChats[parentChatIndex].pinned = singleChat.pinned
+                        }
+                        if tempChats[parentChatIndex].pinned > 0 {
+                            if singleChat.pinned == 0 {
+                                singleChat.pinned = lowestPinned - 1
+                                singleChat.isFolPinned = true
+                            }
+                            tempChats.forEach { chat in
+                                if chat.groupId == singleChat.groupId && (chat.pinned == 0 || (chat.isFolPinned && chat.pinned != lowestPinned - 1)) {
+                                    chat.pinned = lowestPinned - 1
+                                    chat.isFolPinned = true
+                                }
+                            }
+                        }
                     }
                     
                     if let parentExist = chatParentInPreviousChats, parentExist.isSelected,
@@ -913,10 +950,19 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
                         tempChats.insert(singleChat, at: min(indexParent + existingGroup.count, tempChats.count))
                     }
                 } else {
+                    lowestPinned = 0
+                    if singleChat.pinned != 0 {
+                        lowestPinned = singleChat.pinned
+                    }
                     self.chatGroupMaps[singleChat.groupId] = [singleChat]
                     let parentChat = Chat(profile: singleChat.profile, groupName: singleChat.groupName, counter: singleChat.counter, groupId: singleChat.groupId)
                     parentChat.isParent = true
                     
+                    if parentChat.pinned != singleChat.pinned {
+                        parentChat.pinned = singleChat.pinned
+                        self.listMaxArchived[singleChat.groupId] = [singleChat.pin]
+                    }
+                    
                     if let parentExist = chatParentInPreviousChats, parentExist.isSelected {
                         parentChat.isSelected = true
                         tempChats.append(parentChat)
@@ -926,7 +972,10 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
                     }
                 }
             }
-
+            tempChats.sort(by: { $0.pinned > $1.pinned })
+            if self.archivedChats.count > 0 {
+                tempChats.insert(Chat(pin: "Archived"), at: 0)
+            }
             self.chats = tempChats
             completion()
         }
@@ -1096,6 +1145,13 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
             } else {
                 data = chats[indexPath.row]
             }
+            if self.archivedChats.count > 0 && indexPath.row == 0 {
+                let controller = ArchivedChatView()
+                controller.archivedChats = archivedChats
+                controller.hidesBottomBarWhenPushed = true
+                navigationController?.show(controller, sender: nil)
+                return
+            }
             if data.isParent {
                 expandCollapseChats(tableView: tableView, indexPath: indexPath)
                 return
@@ -1157,6 +1213,7 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                         } else {
                             chats.insert(dataSubChat, at: indexParent + 1)
                             indexParent+=1
+                            chats.sort(by: { $0.pinned > $1.pinned })
                         }
                     }
                     
@@ -1384,6 +1441,102 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
         }
     }
     
+    func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+        let data: Chat
+        if isFiltering {
+            data = fillteredData[indexPath.row] as! Chat
+        } else {
+            data = chats[indexPath.row]
+        }
+        if !data.isParent && segment.selectedSegmentIndex == 0 {
+            if self.archivedChats.count > 0 && indexPath.row == 0 {
+                return nil
+            }
+            let archiveAction = UIContextualAction(style: .normal, title: nil) { (_, _, completionHandler) in
+                DispatchQueue.global().async {
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        do {
+                            _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
+                                "pinned" : 0,
+                                "archived" : Date().currentTimeMillis()
+                            ], _where: "l_pin = '\(data.pin)'")
+                        } catch {
+                            rollback.pointee = true
+                            print("Access database error: \(error.localizedDescription)")
+                        }
+                    })
+                }
+                self.chats.remove(at: indexPath.row)
+                tableView.deleteRows(at: [indexPath], with: .right)
+                self.getChats() {
+                    DispatchQueue.main.async {
+                        self.tableView.reloadData()
+                        self.isGettingData = false
+                    }
+                }
+                completionHandler(true)
+            }
+            archiveAction.backgroundColor = .mainColor
+            let archiveIcon = UIImage(systemName: "archivebox.fill")!.createCustomIconWithText(text: "Archive".localized())
+            archiveAction.image = archiveIcon
+
+            var textPinned = "Pin".localized()
+            var pinnedDate = Date().currentTimeMillis()
+            var imagePinned = UIImage(systemName: "pin.fill")!
+            if data.pinned != 0 && !data.isFolPinned {
+                textPinned = "Unpin".localized()
+                pinnedDate = 0
+                imagePinned = UIImage(systemName: "pin.slash.fill")!
+            }
+            let unpinAction = UIContextualAction(style: .normal, title: textPinned) { (_, _, completionHandler) in
+                if pinnedDate != 0 && ((self.listMaxArchived[data.groupId] != nil && self.listMaxArchived[data.groupId]!.count == 3) || (self.listMaxArchived.count == 3)) {
+                    let alertController = LibAlertController(
+                        title: "You can only pin 3 chats".localized(),
+                        message: nil,
+                        preferredStyle: .alert
+                    )
+                    
+                    alertController.addAction(UIAlertAction(title: "OK".localized(), style: .cancel, handler: nil))
+                    
+                    if UIApplication.shared.visibleViewController?.navigationController != nil {
+                        UIApplication.shared.visibleViewController?.navigationController?.present(alertController, animated: true, completion: nil)
+                    } else {
+                        UIApplication.shared.visibleViewController?.present(alertController, animated: true, completion: nil)
+                    }
+                } else {
+                    DispatchQueue.global().async {
+                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                            do {
+                                _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
+                                    "pinned" : pinnedDate
+                                ], _where: "l_pin = '\(data.pin)'")
+                            } catch {
+                                rollback.pointee = true
+                                print("Access database error: \(error.localizedDescription)")
+                            }
+                        })
+                    }
+                    self.chats.remove(at: indexPath.row)
+                    tableView.deleteRows(at: [indexPath], with: .right)
+                    self.getChats() {
+                        DispatchQueue.main.async {
+                            self.tableView.reloadData()
+                            self.isGettingData = false
+                        }
+                    }
+                }
+                completionHandler(true)
+            }
+            unpinAction.backgroundColor = .darkGray
+            let pinIcon = imagePinned.rotateImage(byDegrees: 45).createCustomIconWithText(text: textPinned, color: .white)
+            unpinAction.image = pinIcon
+
+            let configuration = UISwipeActionsConfiguration(actions: [archiveAction, unpinAction])
+            return configuration
+        }
+        return nil
+    }
+    
     func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
         if isFiltering && segment.selectedSegmentIndex == 0 && section == 1 && fillteredMessages.count > 0 {
             return 20
@@ -1482,7 +1635,7 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                 cell.selectionStyle = .none
                 return cell
             }
-            let data: Chat
+            var data: Chat
             if isFiltering {
                 data = fillteredData[indexPath.row] as! Chat
             } else {
@@ -1791,6 +1944,34 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                 return cell
             }
             
+            if self.archivedChats.count > 0 && indexPath.row == 0 {
+                let imageView = UIImageView()
+                content.addSubview(imageView)
+                imageView.translatesAutoresizingMaskIntoConstraints = false
+                NSLayoutConstraint.activate([
+                    imageView.centerYAnchor.constraint(equalTo: content.centerYAnchor),
+                    imageView.widthAnchor.constraint(equalToConstant: 20.0),
+                    imageView.heightAnchor.constraint(equalToConstant: 20.0),
+                    imageView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 25.0)
+                ])
+                imageView.image = UIImage(systemName: "archivebox")!
+                imageView.tintColor = .darkGray
+                
+                let titleView = UILabel()
+                content.addSubview(titleView)
+                titleView.translatesAutoresizingMaskIntoConstraints = false
+                NSLayoutConstraint.activate([
+                    titleView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 25.0),
+                    titleView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -40.0),
+                    titleView.centerYAnchor.constraint(equalTo: content.centerYAnchor),
+                ])
+                titleView.font = UIFont.boldSystemFont(ofSize: 14 + String.offset())
+                titleView.text = "Archived".localized()
+                titleView.textColor = .darkGray
+                cell.backgroundColor = .clear
+                cell.separatorInset = UIEdgeInsets(top: 0, left: 60.0, bottom: 0, right: 0)
+                return cell
+            }
             let imageView = UIImageView()
             content.addSubview(imageView)
             imageView.translatesAutoresizingMaskIntoConstraints = false
@@ -1867,6 +2048,7 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
             
             let timeView = UILabel()
             let viewCounter = UIView()
+            let viewPinned = UIImageView()
             
             if data.counter != "0" {
                 timeView.textColor = .systemRed
@@ -1900,6 +2082,17 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                 labelCounter.textAlignment = .center
             }
             
+            if data.pinned != 0 && !data.isFolPinned {
+                viewPinned.image = UIImage(systemName: "pin.fill")!.rotateImage(byDegrees: 45).withRenderingMode(.alwaysTemplate)
+                viewPinned.tintColor = .darkGray
+                content.addSubview(viewPinned)
+                viewPinned.translatesAutoresizingMaskIntoConstraints = false
+                NSLayoutConstraint.activate([
+                    viewPinned.widthAnchor.constraint(equalToConstant: 18),
+                    viewPinned.heightAnchor.constraint(equalToConstant: 18)
+                ])
+            }
+            
             if !data.isParent {
                 titleView.topAnchor.constraint(equalTo: content.topAnchor, constant: 10.0).isActive = true
                 titleView.text = data.name
@@ -2020,6 +2213,20 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                     viewCounter.topAnchor.constraint(equalTo: timeView.bottomAnchor, constant: 5.0).isActive = true
                     viewCounter.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20).isActive = true
                 }
+                if data.pinned != 0 && !data.isFolPinned {
+                    NSLayoutConstraint.activate([
+                        viewPinned.topAnchor.constraint(equalTo: timeView.bottomAnchor, constant: 5.0)
+                    ])
+                    if data.counter == "0" {
+                        NSLayoutConstraint.activate([
+                            viewPinned.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20)
+                        ])
+                    } else {
+                        NSLayoutConstraint.activate([
+                            viewPinned.trailingAnchor.constraint(equalTo: viewCounter.leadingAnchor, constant: -5)
+                        ])
+                    }
+                }
             } else {
                 titleView.centerYAnchor.constraint(equalTo: content.centerYAnchor).isActive = true
                 titleView.text = data.groupName
@@ -2040,6 +2247,20 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                     viewCounter.trailingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: -5).isActive = true
                     viewCounter.centerYAnchor.constraint(equalTo: content.centerYAnchor).isActive = true
                 }
+                if data.pinned != 0 && !data.isFolPinned {
+                    NSLayoutConstraint.activate([
+                        viewPinned.centerYAnchor.constraint(equalTo: content.centerYAnchor)
+                    ])
+                    if data.counter == "0" {
+                        NSLayoutConstraint.activate([
+                            viewPinned.trailingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: -5)
+                        ])
+                    } else {
+                        NSLayoutConstraint.activate([
+                            viewPinned.trailingAnchor.constraint(equalTo: viewCounter.leadingAnchor, constant: -5)
+                        ])
+                    }
+                }
             }
         case 1:
             cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifierGroup", for: indexPath)
@@ -2241,12 +2462,16 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
             return 130.0
         }
         let fontSize = Int(SecureUserDefaults.shared.value(forKey: "font_size") ?? "0")
+        var finalHeight = 75.0
+        if self.archivedChats.count > 0 && indexPath.row == 0 {
+            finalHeight = 50.0
+        }
         if fontSize == 4 {
-            return 85.0
+            finalHeight += 10
         } else if fontSize == 6 {
-            return 95.0
+            finalHeight += 20
         }
-        return 75.0
+        return finalHeight
     }
     
     private func getRealStatus(messageId: String) -> String {

+ 1 - 0
AppBuilder/AppBuilder/ViewController.swift

@@ -76,6 +76,7 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
     }
     
     override func viewDidDisappear(_ animated: Bool) {
+        Nexilis.floatingButton.hideButton()
         Utils.randomizeBackground(view: self.navigationController?.view)
     }
     

BIN
NexilisLite/.DS_Store


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

@@ -1040,6 +1040,7 @@ public class APIS: NSObject {
     public static var uuidCall: UUID?
     public static var fpinCall: String?
     public static func showNotificationNexilis(_ userInfo: [AnyHashable : Any]) {
+        print("MASUK SHOW NOTIFICATION NEXILIS: \(userInfo)")
         if checkAppStateisBackground() {
 //            Nexilis.sendStateToServer(s: "MASUK SHOW NOTIFICATION NEXILIS")
 //            print("MASUK SHOW NOTIFICATION NEXILIS: \(userInfo)")
@@ -1192,26 +1193,24 @@ public class APIS: NSObject {
     }
     
     private static func ackAPN(id: String) {
-        DispatchQueue.global().async {
+        DispatchQueue.global(qos: .background).async {
 //            Nexilis.sendStateToServer(s: "send ack from apn")
-            DispatchQueue.global().async {
-                let parameter: [String : Any] = [
-                    "pin": User.getMyPin() ?? "",
-                    "message_id": id
-                ]
-                Utils.postDataWithCookiesAndUserAgent(from: URL(string: Utils.getDomainOpr() + "ack_message")!, parameter: parameter, isFormData: true) { data, response, error in
-                }
+            let parameter: [String : Any] = [
+                "pin": User.getMyPin() ?? "",
+                "message_id": id
+            ]
+            Utils.postDataWithCookiesAndUserAgent(from: URL(string: Utils.getDomainOpr() + "ack_message")!, parameter: parameter, isFormData: true, isBackground: true) { data, response, error in
             }
         }
     }
     
     private static func getMessageById(id: String) {
-        DispatchQueue.global().async {
+        DispatchQueue.global(qos: .background).async {
             let parameter: [String : Any] = [
                 "pin": User.getMyPin() ?? "",
                 "message_id": id
             ]
-            Utils.postDataWithCookiesAndUserAgent(from: URL(string: Utils.getDomainOpr() + "pull_notification")!, parameter: parameter, isFormData: true) { data, response, error in
+            Utils.postDataWithCookiesAndUserAgent(from: URL(string: Utils.getDomainOpr() + "pull_notification")!, parameter: parameter, isFormData: true, isBackground: true) { data, response, error in
                 if let data = data {
                     do {
                         if let dataString = String(data: data, encoding: .utf8) {

+ 38 - 0
NexilisLite/NexilisLite/Source/Database.swift

@@ -98,6 +98,8 @@ public class Database {
         database?.inTransaction({(fmdb, rollback) in
             do {
                 try createDatabase(fmdb: fmdb)
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE_SUMMARY", columnName: "pinned", columnType: "INTEGER", defaultValue: "0")
+                addColumnIfNeeded(database: fmdb, tableName: "MESSAGE_SUMMARY", columnName: "archived", columnType: "INTEGER", defaultValue: "0")
                 result = 1
 //                    print("Create Done")
             } catch {
@@ -106,6 +108,40 @@ public class Database {
         return result
     }
     
+    func addColumnIfNeeded(database: FMDatabase, tableName: String, columnName: String, columnType: String, defaultValue: String? = nil) {
+        do {
+            // 1. Check if the column already exists
+            let rs = try database.executeQuery("PRAGMA table_info(\(tableName))", values: nil)
+            var columnExists = false
+            while rs.next() {
+                if let existingColumn = rs.string(forColumn: "name"), existingColumn == columnName {
+                    columnExists = true
+                    break
+                }
+            }
+            rs.close()
+
+            // 2. If it doesn't exist, add the column
+            if !columnExists {
+                var alterSQL = "ALTER TABLE \(tableName) ADD COLUMN \(columnName) \(columnType)"
+                if let defaultValue = defaultValue {
+                    // Wrap string values in single quotes, leave numeric unquoted
+                    if columnType.lowercased().contains("char") || columnType.lowercased().contains("text") {
+                        alterSQL += " DEFAULT '\(defaultValue)'"
+                    } else {
+                        alterSQL += " DEFAULT \(defaultValue)"
+                    }
+                }
+                try database.executeUpdate(alterSQL, values: nil)
+                print("Added column \(columnName)")
+            } else {
+                print("Column \(columnName) already exists")
+            }
+        } catch {
+            print("Error checking or adding column: \(error.localizedDescription)")
+        }
+    }
+    
     func createDatabase(fmdb:FMDatabase) throws -> Void{
         try fmdb.executeUpdate("CREATE TABLE IF NOT EXISTS 'BUDDY' (" +
                                 "'_id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
@@ -394,6 +430,8 @@ public class Database {
                                 "'l_pin' text NOT NULL DEFAULT ''," +
                                 "'message_id' text NOT NULL," +
                                 "'counter' integer NOT NULL default 0," +
+                                "'pinned' integer NOT NULL default 0," +
+                                "'archived' integer NOT NULL default 0," +
                                 "PRIMARY KEY ('l_pin'))", values: nil)
         
         try fmdb.executeUpdate("CREATE TABLE IF NOT EXISTS 'OUTGOING' (" +

+ 142 - 76
NexilisLite/NexilisLite/Source/Extension.swift

@@ -394,6 +394,55 @@ extension UIImage {
         }
     }
     
+    public func createCustomIconWithText(text: String, color: UIColor = .black) -> UIImage {
+        let size = CGSize(width: 60, height: 60)
+
+        UIGraphicsBeginImageContextWithOptions(size, false, 0)
+        defer { UIGraphicsEndImageContext() }
+
+        let iconRect = CGRect(x: 15, y: 5, width: 30, height: 30)
+        self.withRenderingMode(.alwaysTemplate).withTintColor(color).draw(in: iconRect)
+
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.alignment = .center
+
+        let attributes: [NSAttributedString.Key: Any] = [
+            .font: UIFont.systemFont(ofSize: 14),
+            .paragraphStyle: paragraphStyle,
+            .foregroundColor: color
+        ]
+
+        let textRect = CGRect(x: 0, y: 38, width: 60, height: 20)
+        text.draw(in: textRect, withAttributes: attributes)
+
+        return UIGraphicsGetImageFromCurrentImageContext() ?? self
+    }
+    
+    public func rotateImage(byDegrees degrees: CGFloat) -> UIImage {
+        let radians = degrees * CGFloat.pi / 180
+        let newSize = CGRect(origin: .zero, size: CGSize(width: 15, height: 15))
+            .applying(CGAffineTransform(rotationAngle: radians))
+            .integral.size
+
+        UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
+        guard let context = UIGraphicsGetCurrentContext() else { return self }
+
+        // Move origin to center
+        context.translateBy(x: newSize.width / 2, y: newSize.height / 2)
+        // Rotate context
+        context.rotate(by: radians)
+        // Draw the image at the center
+        self.draw(in: CGRect(x: -self.size.width / 2,
+                              y: -self.size.height / 2,
+                              width: self.size.width,
+                              height: self.size.height))
+
+        let rotatedImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+
+        return rotatedImage ?? self
+    }
+    
 }
 
 extension NSObject {
@@ -422,7 +471,7 @@ extension NSObject {
 //                FileEncryption.shared.wipeData(&tempData)
                 completion(true, true, isCircle ? image?.circleMasked : image)
             } else {
-                completion(false, false, placeholderImage)
+//                completion(false, false, placeholderImage)
                 Download().startHTTP(forKey: url) { (name, progress) in
                     guard progress == 100 else {
                         return
@@ -448,12 +497,6 @@ extension NSObject {
                                 }
                             }
                         }
-                        guard let tableView = tableView else { return }
-                        if let indexPath = indexPath,
-                           indexPath.section < tableView.numberOfSections,
-                           indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {
-                            tableView.reloadRows(at: [indexPath], with: .none)
-                        }
                     }
                 }
             }
@@ -466,11 +509,7 @@ extension NSObject {
                 
                 DispatchQueue.main.async {
                     guard let tableView = tableView else { return }
-                    if let indexPath = indexPath,
-                       indexPath.section < tableView.numberOfSections,
-                       indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {
-                        tableView.reloadRows(at: [indexPath], with: .none)
-                    }
+                    tableView.reloadData()
                 }
             }
         }
@@ -532,10 +571,30 @@ extension URL {
             FileTypeSignature(magic: "89504E47", extensions: ["png"]),
             FileTypeSignature(magic: "47494638", extensions: ["gif"]),
             FileTypeSignature(magic: "25504446", extensions: ["pdf"]),
-            FileTypeSignature(magic: "504B0304", extensions: ["zip", "docx", "xlsx", "pptx"]),
-            FileTypeSignature(magic: "D0CF11E0", extensions: ["pptx", "docx", "xlsx", "xls", "doc", "ppt"]), // legacy/enc Office
+            FileTypeSignature(magic: "504B0304", extensions: ["zip", "docx", "xlsx", "pptx", "jar", "odt", "ods", "odp", "apk", "ipa"]),
+            FileTypeSignature(magic: "D0CF11E0", extensions: ["doc", "xls", "ppt", "msg"]), // Legacy MS Office
             FileTypeSignature(magic: "52617221", extensions: ["rar"]),
+            FileTypeSignature(magic: "1F8B08", extensions: ["gz"]),
+            FileTypeSignature(magic: "425A68", extensions: ["bz2"]),
+            FileTypeSignature(magic: "377ABCAF271C", extensions: ["7z"]),
             FileTypeSignature(magic: "00000018", extensions: ["mp4"]),
+            FileTypeSignature(magic: "000001BA", extensions: ["mpg", "mpeg"]),
+            FileTypeSignature(magic: "000001B3", extensions: ["mpg"]),
+            FileTypeSignature(magic: "494433", extensions: ["mp3"]),
+            FileTypeSignature(magic: "4F676753", extensions: ["ogg"]),
+            FileTypeSignature(magic: "1A45DFA3", extensions: ["mkv", "webm"]),
+            FileTypeSignature(magic: "3026B2758E66CF11", extensions: ["wmv", "wma", "asf"]),
+            FileTypeSignature(magic: "57415645", extensions: ["wav"]),
+            FileTypeSignature(magic: "52494646", extensions: ["avi", "wav", "webp"]),
+            FileTypeSignature(magic: "3C3F786D6C", extensions: ["xml"]),
+            FileTypeSignature(magic: "68746D6C3E", extensions: ["html", "htm"]),
+            FileTypeSignature(magic: "7B5C727466", extensions: ["rtf"]),
+            FileTypeSignature(magic: "38425053", extensions: ["psd"]),
+            FileTypeSignature(magic: "49492A00", extensions: ["tif", "tiff"]),
+            FileTypeSignature(magic: "4D4D002A", extensions: ["tif", "tiff"]),
+            FileTypeSignature(magic: "00010000", extensions: ["ico"]),
+            FileTypeSignature(magic: "CAFEBABE", extensions: ["class"]),
+            FileTypeSignature(magic: "EFBBBF", extensions: ["txt"])
         ]
     }
 
@@ -545,17 +604,6 @@ extension URL {
             hexString.hasPrefix($0.magic)
         }
     }
-
-    func isLikelyTextFile(data: Data) -> Bool {
-        let sample = data.prefix(512)
-        for byte in sample {
-            if !(byte >= 0x09 && byte <= 0x0D) &&
-               !(byte >= 0x20 && byte <= 0x7E) {
-                return false
-            }
-        }
-        return true
-    }
     
     func isEncryptedPDF(data: Data) -> Bool {
         if let content = String(data: data.prefix(2048), encoding: .ascii) {
@@ -586,10 +634,6 @@ extension URL {
             return detected.extensions.contains(String(fileExtension))
         }
 
-        if isLikelyTextFile(data: fileData) {
-            return fileExtension == "txt"
-        }
-
         return false
     }
 
@@ -1328,52 +1372,71 @@ public class ImageCache {
     private let cacheGif = NSCache<NSString, NSData>()
     private var cacheKeyMap: [String: String] = [:]
 
+    private let imageCacheDirectory: URL
+    private let gifCacheDirectory: URL
+
     private init() {
-        if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
-            let cacheDirectory = documentsDirectory.appendingPathComponent("fileCacheImage")
-            loadCacheFromDisk(directory: cacheDirectory)
-            
-            let cacheGifDirectory = documentsDirectory.appendingPathComponent("fileCacheGif")
-            loadCacheFromDisk(directory: cacheGifDirectory)
-        }
+        let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
+
+        imageCacheDirectory = documentsDirectory.appendingPathComponent("fileCacheImage")
+        gifCacheDirectory = documentsDirectory.appendingPathComponent("fileCacheGif")
+
+        loadCacheFromDisk(directory: imageCacheDirectory, isGif: false)
+        loadCacheFromDisk(directory: gifCacheDirectory, isGif: true)
     }
 
     public func save(image: UIImage, forKey key: String) {
         let sanitizedKey = sanitizeKey(key)
         cache.setObject(image, forKey: sanitizedKey as NSString)
         cacheKeyMap[key] = sanitizedKey
-        if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
-            let cacheDirectory = documentsDirectory.appendingPathComponent("fileCacheImage")
-            saveCacheToDisk(directory: cacheDirectory)
-        }
+        saveCacheToDisk(directory: imageCacheDirectory, isGif: false)
     }
-    
+
     public func saveGif(data: NSData, forKey key: String) {
         let sanitizedKey = sanitizeKey(key)
         cacheGif.setObject(data, forKey: sanitizedKey as NSString)
         cacheKeyMap[key] = sanitizedKey
-        if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
-            let cacheDirectory = documentsDirectory.appendingPathComponent("fileCacheGif")
-            saveCacheToDisk(directory: cacheDirectory)
-        }
+        saveCacheToDisk(directory: gifCacheDirectory, isGif: true)
     }
 
     public func image(forKey key: String) -> UIImage? {
         let sanitizedKey = sanitizeKey(key)
-        return cache.object(forKey: sanitizedKey as NSString)
+        if let image = cache.object(forKey: sanitizedKey as NSString) {
+            return image
+        }
+
+        // Try loading from disk if not in memory
+        let imageName = "\(sanitizedKey).png"
+        if FileEncryption.shared.isSecureExists(filename: imageName) {
+            do {
+                var data = try FileEncryption.shared.readSecure(filename: imageName)
+                if let decrypted = FileEncryption.shared.decryptFileFromServer(data: data!) {
+                    data = decrypted
+                }
+                if let image = UIImage(data: data!) {
+                    cache.setObject(image, forKey: sanitizedKey as NSString)
+                    return image
+                }
+            } catch {
+                print("Failed to read or decrypt image from disk: \(error)")
+            }
+        }
+
+        return nil
     }
+
     public func imageGif(forKey key: String) -> NSData? {
         let sanitizedKey = sanitizeKey(key)
         return cacheGif.object(forKey: sanitizedKey as NSString)
     }
-    
-    func saveCacheToDisk(directory: URL) {
-        let fileManager = FileManager.default
 
+    private func saveCacheToDisk(directory: URL, isGif: Bool) {
+        let fileManager = FileManager.default
         if !fileManager.fileExists(atPath: directory.path) {
             do {
                 try fileManager.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
             } catch {
+                print("Failed to create cache directory: \(error)")
                 return
             }
         }
@@ -1383,55 +1446,58 @@ public class ImageCache {
             let jsonData = try JSONSerialization.data(withJSONObject: cacheKeyMap, options: [])
             try jsonData.write(to: mappingFilePath)
         } catch {
+            print("Failed to write mapping file: \(error)")
             return
         }
 
         for (_, sanitizedKey) in cacheKeyMap {
-            if let image = cache.object(forKey: sanitizedKey as NSString),
-               let imageData = image.pngData() {
-                do {
-                    try FileEncryption.shared.writeSecure(filename: "\(sanitizedKey).png", data: imageData)
-                } catch {
+            if isGif {
+                if let gifData = cacheGif.object(forKey: sanitizedKey as NSString) {
+                    try? FileEncryption.shared.writeSecure(filename: "\(sanitizedKey).gif", data: gifData as Data)
+                }
+            } else {
+                if let image = cache.object(forKey: sanitizedKey as NSString),
+                   let imageData = image.pngData() {
+                    try? FileEncryption.shared.writeSecure(filename: "\(sanitizedKey).png", data: imageData)
                 }
-                
             }
         }
     }
-    
-    func loadCacheFromDisk(directory: URL) {
-        // Load mapping
+
+    private func loadCacheFromDisk(directory: URL, isGif: Bool) {
         let mappingFilePath = directory.appendingPathComponent("keyMapping.json")
         guard let jsonData = try? Data(contentsOf: mappingFilePath),
               let loadedMapping = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] else {
             return
         }
 
-        if cacheKeyMap.count == 0 {
+        if cacheKeyMap.isEmpty {
             cacheKeyMap = loadedMapping
         }
 
-        // Load images
-        for cacheKey in cacheKeyMap {
-            let imageName = "\(cacheKey.value).png"
-            if FileEncryption.shared.isSecureExists(filename: imageName) {
-                do {
-                    if var data = try FileEncryption.shared.readSecure(filename: imageName) {
-                        let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
-                        if dataDecrypt != nil {
-                            data = dataDecrypt!
-                        }
-                        if let image = UIImage(data: data) {
-                            cache.setObject(image, forKey: cacheKey.value as NSString)
-                        }
-                    }
+        for (_, sanitizedKey) in loadedMapping {
+            let fileExtension = isGif ? "gif" : "png"
+            let fileName = "\(sanitizedKey).\(fileExtension)"
+
+            guard FileEncryption.shared.isSecureExists(filename: fileName) else { continue }
+
+            do {
+                var data = try FileEncryption.shared.readSecure(filename: fileName)
+                if let decrypted = FileEncryption.shared.decryptFileFromServer(data: data!) {
+                    data = decrypted
                 }
-                catch {
-                    print("Error reading secure file")
+
+                if isGif {
+                    cacheGif.setObject(data! as NSData, forKey: sanitizedKey as NSString)
+                } else if let image = UIImage(data: data!) {
+                    cache.setObject(image, forKey: sanitizedKey as NSString)
                 }
+            } catch {
+                print("Error loading \(fileExtension) from disk: \(error)")
             }
         }
     }
-    
+
     private func sanitizeKey(_ key: String) -> String {
         let data = Data(key.utf8)
         var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))

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

@@ -256,8 +256,7 @@ class IncomingThread {
                             if Nexilis.showFB {
                                 Nexilis.floatingButton.removeFromSuperview()
                                 Nexilis.floatingButton = FloatingButton()
-                                let viewController = (UIApplication.shared.windows.first?.rootViewController)!
-                                Nexilis.addFB(viewController: viewController, fromMAB: true)
+                                Nexilis.addFB()
                             }
                             var dataImage: [AnyHashable : Any] = [:]
                             dataImage["name"] = ""

+ 17 - 8
NexilisLite/NexilisLite/Source/Model/Chat.swift

@@ -33,6 +33,8 @@ public class Chat: Model {
     public let groupName: String
     public var isSelected: Bool
     public var isParent: Bool
+    public var pinned: Int
+    public var isFolPinned: Bool
     
     public init(pin: String) {
         self.fpin = ""
@@ -59,6 +61,8 @@ public class Chat: Model {
         self.groupName = ""
         self.isSelected = false
         self.isParent = false
+        self.pinned = 0
+        self.isFolPinned = false
     }
     
     public init(profile: String, groupName: String, counter: String, groupId: String) {
@@ -86,9 +90,11 @@ public class Chat: Model {
         self.groupName = groupName
         self.isSelected = false
         self.isParent = false
+        self.pinned = 0
+        self.isFolPinned = false
     }
     
-    public init(fpin:String, pin: String, messageId: String, counter: String, messageText: String, serverDate: String, image: String, video: String, file: String, attachmentFlag: String, messageScope: String, name: String, profile: String, official: String, status: String, credential: String, lock: String, thumb: String = "", audio: String = "", gif: String = "", groupId: String = "", groupName: String = "", isSelected: Bool = false, isParent: Bool = false) {
+    public init(fpin:String, pin: String, messageId: String, counter: String, messageText: String, serverDate: String, image: String, video: String, file: String, attachmentFlag: String, messageScope: String, name: String, profile: String, official: String, status: String, credential: String, lock: String, thumb: String = "", audio: String = "", gif: String = "", groupId: String = "", groupName: String = "", isSelected: Bool = false, isParent: Bool = false, pinned: Int = 0, isFolPinned: Bool = false) {
         self.fpin = fpin
         self.pin = pin
         self.messageId = messageId
@@ -113,6 +119,8 @@ public class Chat: Model {
         self.groupName = groupName
         self.isSelected = isSelected
         self.isParent = isParent
+        self.pinned = pinned
+        self.isFolPinned = isFolPinned
     }
     
     public static func == (lhs: Chat, rhs: Chat) -> Bool {
@@ -199,7 +207,7 @@ public class Chat: Model {
 //        })
 //    }
     
-    public static func getData(isImage: Bool = false, isDoc: Bool = false, isVideo: Bool = false, isGIF: Bool = false, isLink: Bool = false, isAudio: Bool = false) -> [Chat] {
+    public static func getData(isImage: Bool = false, isDoc: Bool = false, isVideo: Bool = false, isGIF: Bool = false, isLink: Bool = false, isAudio: Bool = false, isArchived: Bool = false) -> [Chat] {
         var chats: [Chat] = []
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
@@ -218,15 +226,15 @@ public class Chat: Model {
                     lastQuery = "m.audio_id IS NOT NULL AND m.audio_id != ''"
                 }
                 var query = """
-                            select m.f_pin, ms.l_pin, ms.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, b.first_name || ' ' || ifnull(b.last_name, '') name, b.image_id profile, b.official_account, m.status, m.credential, m.lock, m.audio_id, m.gif_id, '' group_id, '' group_name from MESSAGE_SUMMARY ms, MESSAGE m, BUDDY b where ms.l_pin = b.f_pin and ms.message_id = m.message_id and m.is_call_center = 0
+                            select m.f_pin, ms.l_pin, ms.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, b.first_name || ' ' || ifnull(b.last_name, '') name, b.image_id profile, b.official_account, m.status, m.credential, m.lock, m.audio_id, m.gif_id, '' group_id, '' group_name, ms.pinned from MESSAGE_SUMMARY ms, MESSAGE m, BUDDY b where ms.l_pin = b.f_pin and ms.message_id = m.message_id and m.is_call_center = 0 \(isArchived ? "and ms.archived <> 0" : "and ms.archived = 0")
                             union
-                            select m.f_pin, ms.l_pin, ms.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, 'Bot' name, '' profile, '', m.status, m.credential, m.lock, m.audio_id, m.gif_id, '' group_id, '' group_name from MESSAGE_SUMMARY ms, MESSAGE m where ms.l_pin = '-999' and ms.message_id = m.message_id
+                            select m.f_pin, ms.l_pin, ms.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, 'Bot' name, '' profile, '', m.status, m.credential, m.lock, m.audio_id, m.gif_id, '' group_id, '' group_name, ms.pinned from MESSAGE_SUMMARY ms, MESSAGE m where ms.l_pin = '-999' and ms.message_id = m.message_id \(isArchived ? "and ms.archived <> 0" : "and ms.archived = 0")
                             union
-                            select m.f_pin, ms.l_pin, ms.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, 'GPT SmartBot' name, '' profile, '', m.status, m.credential, m.lock, m.audio_id, m.gif_id, '' group_id, '' group_name from MESSAGE_SUMMARY ms, MESSAGE m where ms.l_pin = '-997' and ms.message_id = m.message_id
+                            select m.f_pin, ms.l_pin, ms.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, 'GPT SmartBot' name, '' profile, '', m.status, m.credential, m.lock, m.audio_id, m.gif_id, '' group_id, '' group_name, ms.pinned from MESSAGE_SUMMARY ms, MESSAGE m where ms.l_pin = '-997' and ms.message_id = m.message_id \(isArchived ? "and ms.archived <> 0" : "and ms.archived = 0")
                             union
-                            select m.f_pin, ms.l_pin, ms.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, '\("Lounge".localized())' name, b.image_id profile, b.official, m.status, m.credential, m.lock, m.audio_id, m.gif_id, b.group_id, b.f_name group_name from MESSAGE_SUMMARY ms, MESSAGE m, GROUPZ b where ms.l_pin = b.group_id and ms.message_id = m.message_id and m.is_call_center = 0
+                            select m.f_pin, ms.l_pin, ms.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, '\("Lounge".localized())' name, b.image_id profile, b.official, m.status, m.credential, m.lock, m.audio_id, m.gif_id, b.group_id, b.f_name group_name, ms.pinned from MESSAGE_SUMMARY ms, MESSAGE m, GROUPZ b where ms.l_pin = b.group_id and ms.message_id = m.message_id and m.is_call_center = 0 \(isArchived ? "and ms.archived <> 0" : "and ms.archived = 0")
                             union
-                            select m.f_pin, ms.l_pin, ms.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, b.title, c.image_id profile, '', m.status, m.credential, m.lock, m.audio_id, m.gif_id, c.group_id, c.f_name group_name from MESSAGE_SUMMARY ms, MESSAGE m, DISCUSSION_FORUM b, GROUPZ c where b.group_id = c.group_id and ms.l_pin = b.chat_id and ms.message_id = m.message_id and m.is_call_center = 0
+                            select m.f_pin, ms.l_pin, ms.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, b.title, c.image_id profile, '', m.status, m.credential, m.lock, m.audio_id, m.gif_id, c.group_id, c.f_name group_name, ms.pinned from MESSAGE_SUMMARY ms, MESSAGE m, DISCUSSION_FORUM b, GROUPZ c where b.group_id = c.group_id and ms.l_pin = b.chat_id and ms.message_id = m.message_id and m.is_call_center = 0 \(isArchived ? "and ms.archived <> 0" : "and ms.archived = 0")
                             order by 6 desc
                             """
                 if !lastQuery.isEmpty {
@@ -267,7 +275,8 @@ public class Chat: Model {
                                         audio: !lastQuery.isEmpty ? cursorData.string(forColumnIndex: 13) ?? "" : cursorData.string(forColumnIndex: 17) ?? "",
                                         gif: !lastQuery.isEmpty ? cursorData.string(forColumnIndex: 17) ?? "" : cursorData.string(forColumnIndex: 18) ?? "",
                                         groupId: cursorData.string(forColumnIndex: 19) ?? "",
-                                        groupName: cursorData.string(forColumnIndex: 20) ?? "")
+                                        groupName: cursorData.string(forColumnIndex: 20) ?? "",
+                                        pinned: Int(cursorData.int(forColumnIndex: 21)))
                         chats.append(chat)
                     }
                     cursorData.close()

+ 9 - 9
NexilisLite/NexilisLite/Source/Model/Group.swift

@@ -149,13 +149,13 @@ public class Member: User {
         var members: [Member] = []
         Database.shared.database?.inTransaction({ fmdb, rollback in
             do {
-                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name from GROUPZ_MEMBER where group_id = '\(group_id)' OR group_id = (select group_id from DISCUSSION_FORUM where chat_id = '\(group_id)')") {
+                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name, thumb_id, position from GROUPZ_MEMBER where group_id = '\(group_id)' OR group_id = (select group_id from DISCUSSION_FORUM where chat_id = '\(group_id)')") {
                     while cursor.next() {
                         members.append(Member(pin: cursor.string(forColumnIndex: 0) ?? "",
                                               firstName: cursor.string(forColumnIndex: 1) ?? "",
                                               lastName: cursor.string(forColumnIndex: 2) ?? "",
-                                              thumb: "",
-                                              position: ""))
+                                              thumb: cursor.string(forColumnIndex: 3) ?? "",
+                                              position: cursor.string(forColumnIndex: 4) ?? ""))
                     }
                     cursor.close()
                 }
@@ -171,13 +171,13 @@ public class Member: User {
         var member: Member?
         Database.shared.database?.inTransaction({ fmdb, rollback in
             do {
-                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name from GROUPZ_MEMBER where f_pin = '\(f_pin)'") {
+                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name, thumb_id, position from GROUPZ_MEMBER where f_pin = '\(f_pin)'") {
                     while cursor.next() {
                         member = Member(pin: cursor.string(forColumnIndex: 0) ?? "",
                                         firstName: cursor.string(forColumnIndex: 1) ?? "",
                                         lastName: cursor.string(forColumnIndex: 2) ?? "",
-                                        thumb: "",
-                                        position: "")
+                                        thumb: cursor.string(forColumnIndex: 3) ?? "",
+                                        position: cursor.string(forColumnIndex: 4) ?? "")
                     }
                     cursor.close()
                 }
@@ -193,13 +193,13 @@ public class Member: User {
         var member: Member?
         Database.shared.database?.inTransaction({ fmdb, rollback in
             do {
-                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name from GROUPZ_MEMBER where f_pin = '\(f_pin)' AND (group_id = '\(group_id)' OR group_id = (select group_id from DISCUSSION_FORUM where chat_id = '\(group_id)'))") {
+                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name, thumb_id, position from GROUPZ_MEMBER where f_pin = '\(f_pin)' AND (group_id = '\(group_id)' OR group_id = (select group_id from DISCUSSION_FORUM where chat_id = '\(group_id)'))") {
                     while cursor.next() {
                         member = Member(pin: cursor.string(forColumnIndex: 0) ?? "",
                                         firstName: cursor.string(forColumnIndex: 1) ?? "",
                                         lastName: cursor.string(forColumnIndex: 2) ?? "",
-                                        thumb: "",
-                                        position: "")
+                                        thumb: cursor.string(forColumnIndex: 3) ?? "",
+                                        position: cursor.string(forColumnIndex: 4) ?? "")
                     }
                     cursor.close()
                 }

+ 14 - 9
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -19,7 +19,7 @@ import CryptoKit
 import WebKit
 
 public class Nexilis: NSObject {
-    public static var cpaasVersion = "5.0.39"
+    public static var cpaasVersion = "5.0.42"
     public static var sAPIKey = ""
     
     public static var ADDRESS = ""
@@ -184,7 +184,7 @@ public class Nexilis: NSObject {
                             }
                         }
                         if showButton {
-                            addFB(viewController: viewController!, fromMAB: fromMAB)
+                            addFB()
                         }
                         if let rootViewController = viewController {
                             let isDarkMode = rootViewController.traitCollection.userInterfaceStyle == .dark
@@ -246,6 +246,9 @@ public class Nexilis: NSObject {
                 
                 if let me = User.getMyPin() {
                     if Utils.getSetProfile() {
+                        if Utils.getSecureFolderOffline() != "0" {
+                            _ = Database.shared.openDatabase()
+                        }
                         Database.shared.database?.inTransaction({ (fmdb, rollback) in
                             do {
                                 if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT * FROM BUDDY where f_pin = '\(me)' ") {
@@ -393,16 +396,18 @@ public class Nexilis: NSObject {
         Nexilis.sharedAudioPlayer?.stop()
     }
     
-    public static func addFB(viewController: UIViewController, fromMAB: Bool) {
+    public static func addFB() {
         if let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) {
             keyWindow.addSubview(floatingButton)
         }
         if fromMAB {
-            var vc = viewController
-            if viewController is UINavigationController {
-                vc = (viewController as! UINavigationController).rootViewController!
+            if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+               let window = scene.windows.first,
+               let rootVC = window.rootViewController {
+                if let navBar = rootVC as? UINavigationController {
+                    floatingButton.mySettingDelegate = navBar.rootViewController as? any SettingMABDelegate
+                }
             }
-            floatingButton.mySettingDelegate = vc as? any SettingMABDelegate
         }
     }
     
@@ -651,7 +656,7 @@ public class Nexilis: NSObject {
     public static func checkingAccess(key: String) -> Bool {
         let dataAccess = Utils.getFeatureAccess()
         if dataAccess.isEmpty {
-            if key == "sms" || key == "email" || key == "whatsapp" || key == "battery_optimization_force" || key == "backup_restore" || key == "check_sim_swap" || key == "admin_features" || key == "can_config_fb" || key == "friend_request_approval" {
+            if key == "sms" || key == "email" || key == "whatsapp" || key == "battery_optimization_force" || key == "backup_restore" || key == "check_sim_swap" || key == "admin_features" || key == "can_config_fb" || key == "friend_request_approval" || key == "authentication" {
                 return false
             } else {
                 return true
@@ -660,7 +665,7 @@ public class Nexilis: NSObject {
             if jsonArray[key] != nil {
                 return jsonArray[key] as! String == "1"
             } else {
-                if key == "sms" || key == "email" || key == "whatsapp" || key == "battery_optimization_force" || key == "backup_restore" || key == "check_sim_swap" || key == "admin_features" || key == "can_config_fb" || key == "friend_request_approval" {
+                if key == "sms" || key == "email" || key == "whatsapp" || key == "battery_optimization_force" || key == "backup_restore" || key == "check_sim_swap" || key == "admin_features" || key == "can_config_fb" || key == "friend_request_approval" || key == "authentication" {
                     return false
                 } else {
                     return true

+ 6 - 4
NexilisLite/NexilisLite/Source/Utils.swift

@@ -706,7 +706,7 @@ public final class Utils {
         task.resume()
     }
     
-    public static func postDataWithCookiesAndUserAgent(from url: URL, parameter: [String: Any] = [:], parameters: [[String: Any]] = [], isFormData: Bool = false, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
+    public static func postDataWithCookiesAndUserAgent(from url: URL, parameter: [String: Any] = [:], parameters: [[String: Any]] = [], isFormData: Bool = false, isBackground: Bool = false, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
         let apiKey: String = SecureUserDefaults.shared.value(forKey: "apiKey") ?? ""
         var defaultParameter: [String : Any] = [
             "app_id": APIS.getAppNm(),
@@ -740,7 +740,10 @@ public final class Utils {
         }
         request.httpBody = jsonData
         //print("DATA SEND MOBILE \(Utils.getUserAgent()) <> \(Utils.getCookiesMobile())")
-        let urlConfig = URLSessionConfiguration.default
+        var urlConfig = URLSessionConfiguration.default
+        if isBackground{
+            urlConfig = URLSessionConfiguration.background(withIdentifier: "nexilis.backgroundSession")
+        }
         urlConfig.timeoutIntervalForRequest = 30.0
         urlConfig.timeoutIntervalForResource = 60.0
         let sessionDelegate = SelfSignedURLSessionDelegate()
@@ -2158,8 +2161,7 @@ public class DialogVerifyYou: UIViewController {
                                 if Nexilis.showFB {
                                     Nexilis.floatingButton.removeFromSuperview()
                                     Nexilis.floatingButton = FloatingButton()
-                                    let viewController = (UIApplication.shared.windows.first?.rootViewController)!
-                                    Nexilis.addFB(viewController: viewController, fromMAB: true)
+                                    Nexilis.addFB()
                                 }
                                 NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
                                 self.dismiss(animated: true)

+ 489 - 0
NexilisLite/NexilisLite/Source/View/Chat/ArchivedChatView.swift

@@ -0,0 +1,489 @@
+//
+//  ArchivedChatView.swift
+//  Pods
+//
+//  Created by Qindi on 02/06/25.
+//
+
+import Foundation
+import UIKit
+
+public class ArchivedChatView: UIViewController, UITableViewDataSource, UITableViewDelegate {
+    
+    let contactChatNav = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "contactChatNav") as! UINavigationController
+    private let tableView = UITableView(frame: .zero, style: .plain)
+    public var archivedChats: [Chat] = []
+    var archivedChatGroupMaps: [String: [Chat]] = [:]
+    
+    public override func viewDidAppear(_ animated: Bool) {
+        let navBarAppearance = UINavigationBarAppearance()
+        navBarAppearance.configureWithOpaqueBackground()
+        navBarAppearance.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
+        navigationController?.navigationBar.standardAppearance = navBarAppearance
+        navigationController?.navigationBar.scrollEdgeAppearance = navBarAppearance
+        navigationController?.navigationBar.isTranslucent = false
+        navigationController?.navigationBar.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
+        navigationController?.navigationBar.tintColor = .white
+        navigationController?.navigationBar.overrideUserInterfaceStyle = .dark
+        self.setNeedsStatusBarAppearanceUpdate()
+        navigationController?.navigationBar.barStyle = .black
+        if self.navigationController?.isNavigationBarHidden ?? false {
+            self.navigationController?.setNavigationBarHidden(false, animated: false)
+        }
+        navigationController?.navigationBar.prefersLargeTitles = false
+        navigationController?.navigationItem.largeTitleDisplayMode = .never
+    }
+    
+    public override func viewDidLoad() {
+        Utils.addBackground(view: contactChatNav.view)
+        reloadArchiveChat()
+        self.title = "Archived".localized()
+        self.navigationController?.navigationBar.topItem?.title = "".localized()
+        self.navigationController?.navigationBar.setNeedsLayout()
+        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellChatArchived")
+        tableView.dataSource = self
+        tableView.delegate = self
+        tableView.tableFooterView = UIView()
+        
+        setupTableView()
+        
+    }
+    
+    private func reloadArchiveChat() {
+        self.archivedChatGroupMaps.removeAll()
+        let previousChat = self.archivedChats
+        let allChats = Chat.getData(isArchived: true)
+        var tempChats: [Chat] = []
+
+        for singleChat in allChats {
+            guard !singleChat.groupId.isEmpty else {
+                tempChats.append(singleChat)
+                continue
+            }
+            
+            let chatParentInPreviousChats = previousChat.first { $0.isParent && $0.groupId == singleChat.groupId }
+            
+            if var existingGroup = self.archivedChatGroupMaps[singleChat.groupId] {
+                existingGroup.insert(singleChat, at: 0)
+                self.archivedChatGroupMaps[singleChat.groupId] = existingGroup
+                
+                if let parentChatIndex = tempChats.firstIndex(where: { $0.groupId == singleChat.groupId && $0.isParent }) {
+                    if let counterParent = Int(tempChats[parentChatIndex].counter), let counterSingle = Int(singleChat.counter) {
+                        tempChats[parentChatIndex].counter = "\(counterParent + counterSingle)"
+                    }
+                }
+                
+                if let parentExist = chatParentInPreviousChats, parentExist.isSelected,
+                   let indexParent = tempChats.firstIndex(where: { $0.isParent && $0.groupId == singleChat.groupId }) {
+                    tempChats.insert(singleChat, at: min(indexParent + existingGroup.count, tempChats.count))
+                }
+            } else {
+                self.archivedChatGroupMaps[singleChat.groupId] = [singleChat]
+                let parentChat = Chat(profile: singleChat.profile, groupName: singleChat.groupName, counter: singleChat.counter, groupId: singleChat.groupId)
+                parentChat.isParent = true
+                
+                if let parentExist = chatParentInPreviousChats, parentExist.isSelected {
+                    parentChat.isSelected = true
+                    tempChats.append(parentChat)
+                    tempChats.append(singleChat)
+                } else {
+                    tempChats.append(parentChat)
+                }
+            }
+        }
+
+        self.archivedChats = tempChats
+    }
+    
+    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, numberOfRowsInSection section: Int) -> Int {
+        return archivedChats.count
+    }
+    
+    public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        let fontSize = Int(SecureUserDefaults.shared.value(forKey: "font_size") ?? "0")
+        var finalHeight = 75.0
+        if fontSize == 4 {
+            finalHeight += 10
+        } else if fontSize == 6 {
+            finalHeight += 20
+        }
+        return finalHeight
+    }
+    
+    public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        tableView.deselectRow(at: indexPath, animated: true)
+        let data = archivedChats[indexPath.row]
+        if data.isParent {
+            expandCollapseChats(tableView: tableView, indexPath: indexPath)
+            return
+        }
+        if data.pin == "-997" {
+            let smartChatVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "chatGptVC") as! ChatGPTBotView
+            smartChatVC.hidesBottomBarWhenPushed = true
+            smartChatVC.fromNotification = false
+            navigationController?.show(smartChatVC, sender: nil)
+        } else if data.messageScope == MessageScope.WHISPER || data.messageScope == MessageScope.CALL || data.messageScope == MessageScope.MISSED_CALL {
+            if data.pin.isEmpty {
+                return
+            }
+            let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
+            editorPersonalVC.hidesBottomBarWhenPushed = true
+            editorPersonalVC.unique_l_pin = data.pin
+            navigationController?.show(editorPersonalVC, sender: nil)
+        } else {
+            if data.pin.isEmpty {
+                return
+            }
+            let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
+            editorGroupVC.hidesBottomBarWhenPushed = true
+            editorGroupVC.unique_l_pin = data.pin
+            navigationController?.show(editorGroupVC, sender: nil)
+        }
+    }
+    
+    func expandCollapseChats(tableView: UITableView, indexPath: IndexPath) {
+        let data = archivedChats[indexPath.row]
+        data.isSelected = !data.isSelected
+        if data.isSelected {
+            if let dataSubChats = self.archivedChatGroupMaps[data.groupId] {
+                for dataSubChat in dataSubChats {
+                    if var indexParent = archivedChats.firstIndex(where: { $0.isParent && $0.groupId == data.groupId }) {
+                        archivedChats.insert(dataSubChat, at: indexParent + 1)
+                        indexParent+=1
+                    }
+                    
+                }
+            }
+        } else {
+            archivedChats.removeAll(where: { $0.isParent == false && $0.groupId == data.groupId })
+        }
+        tableView.reloadData()
+    }
+    
+    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let cell = tableView.dequeueReusableCell(withIdentifier: "cellChatArchived", for: indexPath)
+        let content = cell.contentView
+        if content.subviews.count > 0 {
+            content.subviews.forEach { $0.removeFromSuperview() }
+        }
+        let data = archivedChats[indexPath.row]
+        let imageView = UIImageView()
+        content.addSubview(imageView)
+        imageView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            imageView.centerYAnchor.constraint(equalTo: content.centerYAnchor),
+            imageView.widthAnchor.constraint(equalToConstant: 55.0),
+            imageView.heightAnchor.constraint(equalToConstant: 55.0)
+        ])
+        var leadingAnchor = imageView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 10.0)
+        if data.pin == "-997" {
+            imageView.frame = CGRect(x: 0, y: 0, width: 55.0, height: 55.0)
+            imageView.circle()
+            if let urlGif = Bundle.resourceBundle(for: Nexilis.self).url(forResource: "pb_gpt_bot", withExtension: "gif") {
+                imageView.sd_setImage(with: urlGif) { (image, error, cacheType, imageURL) in
+                    if error == nil {
+                        imageView.animationImages = image?.images
+                        imageView.animationDuration = image?.duration ?? 0.0
+                        imageView.animationRepeatCount = 0
+                        imageView.startAnimating()
+                    }
+                }
+            } else if let urlGif = Bundle.resourcesMediaBundle(for: Nexilis.self).url(forResource: "pb_gpt_bot", withExtension: "gif") {
+                imageView.sd_setImage(with: urlGif) { (image, error, cacheType, imageURL) in
+                    if error == nil {
+                        imageView.animationImages = image?.images
+                        imageView.animationDuration = image?.duration ?? 0.0
+                        imageView.animationRepeatCount = 0
+                        imageView.startAnimating()
+                    }
+                }
+            }
+        } else {
+            if !Utils.getIconDock().isEmpty && data.official == "1" {
+                let urlString = Utils.getUrlDock()!
+                if let cachedImage = ImageCache.shared.image(forKey: urlString) {
+                    let imageData = cachedImage
+                    imageView.image = imageData
+                } else {
+                    DispatchQueue.global().async{
+                        Utils.fetchDataWithCookiesAndUserAgent(from: URL(string: urlString)!) { data, response, error in
+                            guard let data = data, error == nil else { return }
+                            DispatchQueue.main.async() {
+                                if UIImage(data: data) != nil {
+                                    let imageData = UIImage(data: data)!
+                                    imageView.image = imageData
+                                    ImageCache.shared.save(image: imageData, forKey: urlString)
+                                }
+                            }
+                        }
+                    }
+                }
+            } else {
+                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 {
+                    leadingAnchor = imageView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 40.0)
+                    let image = UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+                    imageView.image = image
+                }
+            }
+        }
+        leadingAnchor.isActive = true
+        
+        let titleView = UILabel()
+        content.addSubview(titleView)
+        titleView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            titleView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10.0),
+            titleView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -40.0),
+        ])
+        titleView.font = UIFont.systemFont(ofSize: 14 + String.offset(), weight: .medium)
+        
+        let timeView = UILabel()
+        let viewCounter = UIView()
+        
+        if data.counter != "0" {
+            timeView.textColor = .systemRed
+            content.addSubview(viewCounter)
+            viewCounter.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                viewCounter.widthAnchor.constraint(greaterThanOrEqualToConstant: 20),
+                viewCounter.heightAnchor.constraint(equalToConstant: 20)
+            ])
+            viewCounter.backgroundColor = .systemRed
+            viewCounter.layer.cornerRadius = 10
+            viewCounter.clipsToBounds = true
+            viewCounter.layer.borderWidth = 0.5
+            viewCounter.layer.borderColor = UIColor.secondaryColor.cgColor
+
+            let labelCounter = UILabel()
+            viewCounter.addSubview(labelCounter)
+            labelCounter.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                labelCounter.centerYAnchor.constraint(equalTo: viewCounter.centerYAnchor),
+                labelCounter.leadingAnchor.constraint(equalTo: viewCounter.leadingAnchor, constant: 2),
+                labelCounter.trailingAnchor.constraint(equalTo: viewCounter.trailingAnchor, constant: -2),
+            ])
+            labelCounter.font = UIFont.systemFont(ofSize: 11 + String.offset())
+            if Int(data.counter)! > 99 {
+                labelCounter.text = "99+"
+            } else {
+                labelCounter.text = data.counter
+            }
+            labelCounter.textColor = .secondaryColor
+            labelCounter.textAlignment = .center
+        }
+        
+        if !data.isParent {
+            titleView.topAnchor.constraint(equalTo: content.topAnchor, constant: 10.0).isActive = true
+            titleView.text = data.name
+            
+            content.addSubview(timeView)
+            timeView.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                timeView.topAnchor.constraint(equalTo: content.topAnchor, constant: 10.0),
+                timeView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20.0),
+            ])
+            timeView.textColor = .gray
+            timeView.font = UIFont.systemFont(ofSize: 14 + String.offset())
+            
+            let date = Date(milliseconds: Int64(data.serverDate) ?? 0)
+            let calendar = Calendar.current
+            
+            if (calendar.isDateInToday(date)) {
+                let formatter = DateFormatter()
+                formatter.dateFormat = "HH:mm"
+                formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
+                timeView.text = 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 {
+                    timeView.text = "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?
+                        }
+                        timeView.text = 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)
+                        timeView.text = stringFormat
+                    }
+                }
+            }
+            
+            let messageView = UILabel()
+            content.addSubview(messageView)
+            messageView.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                messageView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10.0),
+                messageView.topAnchor.constraint(equalTo: titleView.bottomAnchor),
+                messageView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -40.0),
+            ])
+            messageView.textColor = .gray
+            if data.messageText.contains("■") {
+                data.messageText = data.messageText.components(separatedBy: "■")[0]
+                data.messageText = data.messageText.trimmingCharacters(in: .whitespacesAndNewlines)
+            }
+            let text = Utils.previewMessageText(chat: data)
+            let idMe = User.getMyPin() as String?
+            if let attributeText = text as? NSMutableAttributedString {
+                let stringMessage = NSMutableAttributedString(string: "")
+                if data.fpin == idMe {
+                    if data.lock == "1" {
+                        if data.messageScope == "4" {
+                            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 {
+                        let imageStatus = NSTextAttachment()
+                        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: " "))
+                        }
+                        if data.messageScope == "4" {
+                            stringMessage.append(NSAttributedString(string: "You".localized() + ": ", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12 + String.offset(), weight: .medium)]))
+                        }
+                        stringMessage.append(attributeText)
+                    }
+                } else {
+                    if data.messageScope == "4" {
+                        var fullname = User.getData(pin: data.fpin, lPin: data.pin)!.fullName
+                        let components = fullname.split(separator: " ")
+                        if components.count >= 2 {
+                            fullname = components.prefix(2).joined(separator: " ")
+                        }
+                        stringMessage.append(NSAttributedString(string: fullname + ": ", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12 + String.offset(), weight: .medium)]))
+                    }
+                    if data.lock == "1" {
+                        stringMessage.append(("🚫 _"+"This message was deleted".localized()+"_").richText())
+                    } else {
+                        stringMessage.append(attributeText)
+                    }
+                }
+                messageView.attributedText = stringMessage
+            }
+            messageView.numberOfLines = 2
+            
+            if data.counter != "0" {
+                viewCounter.topAnchor.constraint(equalTo: timeView.bottomAnchor, constant: 5.0).isActive = true
+                viewCounter.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20).isActive = true
+            }
+        } else {
+            titleView.centerYAnchor.constraint(equalTo: content.centerYAnchor).isActive = true
+            titleView.text = data.groupName
+            
+            let iconName = (data.isSelected) ? "chevron.up.circle" : "chevron.down.circle"
+            let imageView = UIImageView(image: UIImage(systemName: iconName))
+            imageView.tintColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
+            content.addSubview(imageView)
+            imageView.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                imageView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
+                imageView.centerYAnchor.constraint(equalTo: content.centerYAnchor),
+                imageView.widthAnchor.constraint(equalToConstant: 20),
+                imageView.heightAnchor.constraint(equalToConstant: 20)
+            ])
+            
+            if data.counter != "0" {
+                viewCounter.trailingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: -5).isActive = true
+                viewCounter.centerYAnchor.constraint(equalTo: content.centerYAnchor).isActive = true
+            }
+        }
+        cell.backgroundColor = .clear
+        cell.separatorInset = UIEdgeInsets(top: 0, left: 60.0, bottom: 0, right: 0)
+        return cell
+    }
+    
+    public func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+        let data = archivedChats[indexPath.row]
+        if !data.isParent {
+            let archiveAction = UIContextualAction(style: .normal, title: nil) { (_, _, completionHandler) in
+                DispatchQueue.global().async {
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        do {
+                            _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
+                                "archived" : 0
+                            ], _where: "l_pin = '\(data.pin)'")
+                        } catch {
+                            rollback.pointee = true
+                            print("Access database error: \(error.localizedDescription)")
+                        }
+                    })
+                }
+                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
+                self.archivedChats.remove(at: indexPath.row)
+                tableView.deleteRows(at: [indexPath], with: .right)
+                self.reloadArchiveChat()
+                tableView.reloadData()
+                if self.archivedChats.count == 0 {
+                    self.navigationController?.popViewController(animated: true)
+                }
+                completionHandler(true)
+            }
+            archiveAction.backgroundColor = .mainColor
+            let archiveIcon = UIImage(systemName: "arrow.up.bin.fill")!.createCustomIconWithText(text: "Unarchive".localized())
+            archiveAction.image = archiveIcon
+
+            let configuration = UISwipeActionsConfiguration(actions: [archiveAction])
+            return configuration
+        }
+        return nil
+    }
+    
+    private func getRealStatus(messageId: String) -> String {
+        var status = "1"
+        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+            if let cursorStatus = Database.shared.getRecords(fmdb: fmdb, query: "SELECT status, f_pin FROM MESSAGE_STATUS WHERE message_id='\(messageId)'") {
+                var listStatus: [Int] = []
+                while cursorStatus.next() {
+                    listStatus.append(Int(cursorStatus.string(forColumnIndex: 0)!)!)
+                }
+                cursorStatus.close()
+                status = "\(listStatus.min() ?? 2)"
+            }
+        })
+        return status
+    }
+    
+    
+}

+ 4 - 3
NexilisLite/NexilisLite/Source/View/Chat/ChatWALikeVC.swift

@@ -325,12 +325,13 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
             return 130.0
         }
         let fontSize = Int(SecureUserDefaults.shared.value(forKey: "font_size") ?? "0")
+        var finalHeight = 75.0
         if fontSize == 4 {
-            return 85.0
+            finalHeight += 10
         } else if fontSize == 6 {
-            return 95.0
+            finalHeight += 20
         }
-        return 75.0
+        return finalHeight
     }
     
     public func scrollViewDidScroll(_ scrollView: UIScrollView) {

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

@@ -4472,9 +4472,9 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
             guard indexPath.row < dataMessages.count else {
                 return
             }
-            if copySession && dataMessages[indexPath.row]["f_pin"]  as? String ?? "" != "-999" {
-                return
-            }
+//            if copySession && dataMessages[indexPath.row]["f_pin"]  as? String ?? "" != "-999" {
+//                return
+//            }
             if (dataMessages[indexPath.row]["attachment_flag"]  as? String ?? "" != "0" || dataMessages[indexPath.row]["lock"] as? String == "1") && !forwardSession && !deleteSession {
                 return
             }

+ 12 - 6
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -5321,10 +5321,13 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
                             let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == groupingImages[i].messageId })
                             if idx != nil {
                                 self.dataMessages.remove(at: idx!)
-                                if (idx == self.dataMessages.count - 1) {
-                                    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
-                                }
+//                                if (idx == self.dataMessages.count - 1) {
+//                                    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
+//                                }
                                 for i in 0..<dataDates.count {
+                                    if i > dataDates.count - 1 {
+                                        continue
+                                    }
                                     if self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[i] }).count == 0 {
                                         dataDates.remove(at: i)
                                     }
@@ -5337,10 +5340,13 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
                         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!)
-                            if (idx == self.dataMessages.count - 1) {
-                                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
-                            }
+//                            if (idx == self.dataMessages.count - 1) {
+//                                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
+//                            }
                             for i in 0..<dataDates.count {
+                                if i > dataDates.count - 1 {
+                                    continue
+                                }
                                 if self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[i] }).count == 0 {
                                     dataDates.remove(at: i)
                                 }

+ 3 - 6
NexilisLite/NexilisLite/Source/View/Control/ChangeDeviceViewController.swift

@@ -161,8 +161,7 @@ public class ChangeDeviceViewController: UIViewController {
                                     if Nexilis.showFB {
                                         Nexilis.floatingButton.removeFromSuperview()
                                         Nexilis.floatingButton = FloatingButton()
-                                        let viewController = (UIApplication.shared.windows.first?.rootViewController)!
-                                        Nexilis.addFB(viewController: viewController, fromMAB: true)
+                                        Nexilis.addFB()
                                     }
                                     if self.fromChangeNamePass{
                                         var vc = self.navigationController?.presentingViewController
@@ -285,8 +284,7 @@ public class ChangeDeviceViewController: UIViewController {
                                 if Nexilis.showFB {
                                     Nexilis.floatingButton.removeFromSuperview()
                                     Nexilis.floatingButton = FloatingButton()
-                                    let viewController = (UIApplication.shared.windows.first?.rootViewController)!
-                                    Nexilis.addFB(viewController: viewController, fromMAB: true)
+                                    Nexilis.addFB()
                                 }
                                 NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
                                 if self.fromChangeNamePass{
@@ -327,8 +325,7 @@ public class ChangeDeviceViewController: UIViewController {
                                 if Nexilis.showFB {
                                     Nexilis.floatingButton.removeFromSuperview()
                                     Nexilis.floatingButton = FloatingButton()
-                                    let viewController = (UIApplication.shared.windows.first?.rootViewController)!
-                                    Nexilis.addFB(viewController: viewController, fromMAB: true)
+                                    Nexilis.addFB()
                                 }
                                 NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
                                 if self.fromChangeNamePass{

+ 1 - 2
NexilisLite/NexilisLite/Source/View/Control/ConfigureFloatingButton.swift

@@ -118,8 +118,7 @@ public class ConfigureFloatingButton: UIViewController {
         DispatchQueue.main.async {
             Nexilis.floatingButton.removeFromSuperview()
             Nexilis.floatingButton = FloatingButton()
-            let viewController = (UIApplication.shared.windows.first?.rootViewController)!
-            Nexilis.addFB(viewController: viewController, fromMAB: Nexilis.fromMAB)
+            Nexilis.addFB()
             if Nexilis.fromMAB {
                 Nexilis.floatingButton.isHidden = true
             }

+ 259 - 26
NexilisLite/NexilisLite/Source/View/Control/ContactChatViewController.swift

@@ -23,6 +23,9 @@ class ContactChatViewController: UITableViewController {
     
     var chats: [Chat] = []
     
+    var archivedChats: [Chat] = []
+    var listMaxArchived: [String: [String]] = [:]
+    
     var chatGroupMaps: [String: [Chat]] = [:]
     
     var contacts: [User] = []
@@ -392,7 +395,6 @@ class ContactChatViewController: UITableViewController {
         if self.isGettingData {
             return
         }
-        self.isGettingData = true
         getChats {
             self.getContacts {
                 self.getGroups { g1 in
@@ -418,43 +420,98 @@ class ContactChatViewController: UITableViewController {
     
     func getChats(completion: @escaping ()->()) {
         DispatchQueue.global().async {
+//            while self.isGettingData {
+//                Thread.sleep(forTimeInterval: 0.1)
+//            }
+            self.isGettingData = true
             self.chatGroupMaps.removeAll()
+            self.listMaxArchived.removeAll()
             let previousChat = self.chats
             let allChats = Chat.getData()
+            self.archivedChats = Chat.getData(isArchived: true)
             var tempChats: [Chat] = []
+            var lowestPinned = 0
+
             for singleChat in allChats {
-                if !singleChat.groupId.isEmpty {
-                    let chatParentInPreviousChats = previousChat.first(where: { $0.isParent && $0.groupId == singleChat.groupId })
-                    if self.chatGroupMaps[singleChat.groupId] != nil {
-                        self.chatGroupMaps[singleChat.groupId]!.insert(singleChat, at: 0)
-                        if let parentChat = tempChats.first(where: { $0.groupId == singleChat.groupId && $0.isParent }) {
-                            let counterParent = parentChat.counter
-                            parentChat.counter = "\(Int(counterParent)! + Int(singleChat.counter)!)"
+                guard !singleChat.groupId.isEmpty else {
+                    tempChats.append(singleChat)
+                    if singleChat.pinned > 0 {
+                        self.listMaxArchived[singleChat.pin] = [""]
+                    }
+                    continue
+                }
+                
+                let chatParentInPreviousChats = previousChat.first { $0.isParent && $0.groupId == singleChat.groupId }
+                
+                if var existingGroup = self.chatGroupMaps[singleChat.groupId] {
+                    existingGroup.insert(singleChat, at: 0)
+                    self.chatGroupMaps[singleChat.groupId] = existingGroup
+                    
+                    if let parentChatIndex = tempChats.firstIndex(where: { $0.groupId == singleChat.groupId && $0.isParent }) {
+                        if let counterParent = Int(tempChats[parentChatIndex].counter), let counterSingle = Int(singleChat.counter) {
+                            tempChats[parentChatIndex].counter = "\(counterParent + counterSingle)"
+                        }
+                        if singleChat.pinned != 0 && (lowestPinned == 0 || lowestPinned > singleChat.pinned) {
+                            lowestPinned = singleChat.pinned
                         }
-                        if let parentExist = chatParentInPreviousChats, parentExist.isSelected {
-                            if let indexParent = tempChats.firstIndex(where: { $0.isParent && $0.groupId == singleChat.groupId }){
-                                tempChats.insert(singleChat, at: indexParent + self.chatGroupMaps[singleChat.groupId]!.count)
+                        if singleChat.pinned != 0 {
+                            if !self.listMaxArchived.keys.contains(singleChat.groupId) {
+                                self.listMaxArchived[singleChat.groupId] = [singleChat.pin]
+                            } else {
+                                self.listMaxArchived[singleChat.groupId]?.append(singleChat.pin)
                             }
                         }
-                    } else {
-                        self.chatGroupMaps[singleChat.groupId] = [singleChat]
-                        let parentChat = Chat(profile: singleChat.profile, groupName: singleChat.groupName, counter: singleChat.counter, groupId: singleChat.groupId)
-                        parentChat.isParent = true
-                        if let parentExist = chatParentInPreviousChats, parentExist.isSelected {
-                            parentChat.isSelected = true
-                            tempChats.append(parentChat)
-                            tempChats.append(singleChat)
-                        } else {
-                            tempChats.append(parentChat)
+                        if tempChats[parentChatIndex].pinned < singleChat.pinned {
+                            tempChats[parentChatIndex].pinned = singleChat.pinned
+                        }
+                        if tempChats[parentChatIndex].pinned > 0 {
+                            if singleChat.pinned == 0 {
+                                singleChat.pinned = lowestPinned - 1
+                                singleChat.isFolPinned = true
+                            }
+                            tempChats.forEach { chat in
+                                if chat.groupId == singleChat.groupId && (chat.pinned == 0 || (chat.isFolPinned && chat.pinned != lowestPinned - 1)) {
+                                    chat.pinned = lowestPinned - 1
+                                    chat.isFolPinned = true
+                                }
+                            }
                         }
                     }
+                    
+                    if let parentExist = chatParentInPreviousChats, parentExist.isSelected,
+                       let indexParent = tempChats.firstIndex(where: { $0.isParent && $0.groupId == singleChat.groupId }) {
+                        tempChats.insert(singleChat, at: min(indexParent + existingGroup.count, tempChats.count))
+                    }
                 } else {
-                    tempChats.append(singleChat)
+                    lowestPinned = 0
+                    if singleChat.pinned != 0 {
+                        lowestPinned = singleChat.pinned
+                    }
+                    self.chatGroupMaps[singleChat.groupId] = [singleChat]
+                    let parentChat = Chat(profile: singleChat.profile, groupName: singleChat.groupName, counter: singleChat.counter, groupId: singleChat.groupId)
+                    parentChat.isParent = true
+                    
+                    if parentChat.pinned != singleChat.pinned {
+                        parentChat.pinned = singleChat.pinned
+                        self.listMaxArchived[singleChat.groupId] = [singleChat.pin]
+                    }
+                    
+                    if let parentExist = chatParentInPreviousChats, parentExist.isSelected {
+                        parentChat.isSelected = true
+                        tempChats.append(parentChat)
+                        tempChats.append(singleChat)
+                    } else {
+                        tempChats.append(parentChat)
+                    }
                 }
             }
             if self.isChooser != nil {
                 tempChats.removeAll(where: { $0.pin == "-997" })
             }
+            tempChats.sort(by: { $0.pinned > $1.pinned })
+            if self.archivedChats.count > 0 {
+                tempChats.insert(Chat(pin: "Archived"), at: 0)
+            }
             self.chats = tempChats
             completion()
         }
@@ -700,6 +757,13 @@ extension ContactChatViewController {
                 } else {
                     data = chats[indexPath.row]
                 }
+                if self.archivedChats.count > 0 && indexPath.row == 0 {
+                    let controller = ArchivedChatView()
+                    controller.archivedChats = archivedChats
+                    controller.hidesBottomBarWhenPushed = true
+                    navigationController?.show(controller, sender: nil)
+                    return
+                }
                 if data.isParent {
                     expandCollapseChats(tableView: tableView, indexPath: indexPath)
                     return
@@ -784,6 +848,7 @@ extension ContactChatViewController {
                         } else {
                             chats.insert(dataSubChat, at: indexParent + 1)
                             indexParent+=1
+                            chats.sort(by: { $0.pinned > $1.pinned })
                         }
                     }
                     
@@ -1082,6 +1147,102 @@ extension ContactChatViewController {
         return value
     }
     
+    override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+        let data: Chat
+        if isFilltering {
+            data = fillteredData[indexPath.row] as! Chat
+        } else {
+            data = chats[indexPath.row]
+        }
+        if !data.isParent && segment.selectedSegmentIndex == 0 {
+            if self.archivedChats.count > 0 && indexPath.row == 0 {
+                return nil
+            }
+            let archiveAction = UIContextualAction(style: .normal, title: nil) { (_, _, completionHandler) in
+                DispatchQueue.global().async {
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        do {
+                            _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
+                                "pinned" : 0,
+                                "archived" : Date().currentTimeMillis()
+                            ], _where: "l_pin = '\(data.pin)'")
+                        } catch {
+                            rollback.pointee = true
+                            print("Access database error: \(error.localizedDescription)")
+                        }
+                    })
+                }
+                self.chats.remove(at: indexPath.row)
+                tableView.deleteRows(at: [indexPath], with: .right)
+                self.getChats() {
+                    DispatchQueue.main.async {
+                        self.tableView.reloadData()
+                        self.isGettingData = false
+                    }
+                }
+                completionHandler(true)
+            }
+            archiveAction.backgroundColor = .mainColor
+            let archiveIcon = UIImage(systemName: "archivebox.fill")!.createCustomIconWithText(text: "Archive".localized())
+            archiveAction.image = archiveIcon
+
+            var textPinned = "Pin".localized()
+            var pinnedDate = Date().currentTimeMillis()
+            var imagePinned = UIImage(systemName: "pin.fill")!
+            if data.pinned != 0 && !data.isFolPinned {
+                textPinned = "Unpin".localized()
+                pinnedDate = 0
+                imagePinned = UIImage(systemName: "pin.slash.fill")!
+            }
+            let unpinAction = UIContextualAction(style: .normal, title: textPinned) { (_, _, completionHandler) in
+                if pinnedDate != 0 && ((self.listMaxArchived[data.groupId] != nil && self.listMaxArchived[data.groupId]!.count == 3) || (self.listMaxArchived.count == 3)) {
+                    let alertController = LibAlertController(
+                        title: "You can only pin 3 chats".localized(),
+                        message: nil,
+                        preferredStyle: .alert
+                    )
+                    
+                    alertController.addAction(UIAlertAction(title: "OK".localized(), style: .cancel, handler: nil))
+                    
+                    if UIApplication.shared.visibleViewController?.navigationController != nil {
+                        UIApplication.shared.visibleViewController?.navigationController?.present(alertController, animated: true, completion: nil)
+                    } else {
+                        UIApplication.shared.visibleViewController?.present(alertController, animated: true, completion: nil)
+                    }
+                } else {
+                    DispatchQueue.global().async {
+                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                            do {
+                                _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
+                                    "pinned" : pinnedDate
+                                ], _where: "l_pin = '\(data.pin)'")
+                            } catch {
+                                rollback.pointee = true
+                                print("Access database error: \(error.localizedDescription)")
+                            }
+                        })
+                    }
+                    self.chats.remove(at: indexPath.row)
+                    tableView.deleteRows(at: [indexPath], with: .right)
+                    self.getChats() {
+                        DispatchQueue.main.async {
+                            self.tableView.reloadData()
+                            self.isGettingData = false
+                        }
+                    }
+                }
+                completionHandler(true)
+            }
+            unpinAction.backgroundColor = .darkGray
+            let pinIcon = imagePinned.rotateImage(byDegrees: 45).createCustomIconWithText(text: textPinned, color: .white)
+            unpinAction.image = pinIcon
+
+            let configuration = UISwipeActionsConfiguration(actions: [archiveAction, unpinAction])
+            return configuration
+        }
+        return nil
+    }
+    
     override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         var cell: UITableViewCell!
         switch segment.selectedSegmentIndex {
@@ -1175,7 +1336,7 @@ extension ContactChatViewController {
                     cell.selectionStyle = .none
                     return cell
                 }
-                let data: Chat
+                var data: Chat
                 if isFilltering {
                     data = fillteredData[indexPath.row] as! Chat
                 } else {
@@ -1192,6 +1353,34 @@ extension ContactChatViewController {
                     }
                     data = chats[indexPath.row]
                 }
+                if self.archivedChats.count > 0 && indexPath.row == 0 {
+                    let imageView = UIImageView()
+                    content.addSubview(imageView)
+                    imageView.translatesAutoresizingMaskIntoConstraints = false
+                    NSLayoutConstraint.activate([
+                        imageView.centerYAnchor.constraint(equalTo: content.centerYAnchor),
+                        imageView.widthAnchor.constraint(equalToConstant: 20.0),
+                        imageView.heightAnchor.constraint(equalToConstant: 20.0),
+                        imageView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 25.0)
+                    ])
+                    imageView.image = UIImage(systemName: "archivebox")!
+                    imageView.tintColor = .darkGray
+                    
+                    let titleView = UILabel()
+                    content.addSubview(titleView)
+                    titleView.translatesAutoresizingMaskIntoConstraints = false
+                    NSLayoutConstraint.activate([
+                        titleView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 25.0),
+                        titleView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -40.0),
+                        titleView.centerYAnchor.constraint(equalTo: content.centerYAnchor),
+                    ])
+                    titleView.font = UIFont.boldSystemFont(ofSize: 14 + String.offset())
+                    titleView.text = "Archived".localized()
+                    titleView.textColor = .darkGray
+                    cell.backgroundColor = .clear
+                    cell.separatorInset = UIEdgeInsets(top: 0, left: 60.0, bottom: 0, right: 0)
+                    return cell
+                }
                 let imageView = UIImageView()
                 content.addSubview(imageView)
                 imageView.translatesAutoresizingMaskIntoConstraints = false
@@ -1268,6 +1457,7 @@ extension ContactChatViewController {
                 
                 let timeView = UILabel()
                 let viewCounter = UIView()
+                let viewPinned = UIImageView()
                 
                 if data.counter != "0" {
                     timeView.textColor = .systemRed
@@ -1301,6 +1491,17 @@ extension ContactChatViewController {
                     labelCounter.textAlignment = .center
                 }
                 
+                if data.pinned != 0 && !data.isFolPinned {
+                    viewPinned.image = UIImage(systemName: "pin.fill")!.rotateImage(byDegrees: 45).withRenderingMode(.alwaysTemplate)
+                    viewPinned.tintColor = .darkGray
+                    content.addSubview(viewPinned)
+                    viewPinned.translatesAutoresizingMaskIntoConstraints = false
+                    NSLayoutConstraint.activate([
+                        viewPinned.widthAnchor.constraint(equalToConstant: 18),
+                        viewPinned.heightAnchor.constraint(equalToConstant: 18)
+                    ])
+                }
+                
                 if !data.isParent {
                     titleView.topAnchor.constraint(equalTo: content.topAnchor, constant: 10.0).isActive = true
                     titleView.text = data.name
@@ -1419,6 +1620,20 @@ extension ContactChatViewController {
                         viewCounter.topAnchor.constraint(equalTo: timeView.bottomAnchor, constant: 5.0).isActive = true
                         viewCounter.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20).isActive = true
                     }
+                    if data.pinned != 0 && !data.isFolPinned {
+                        NSLayoutConstraint.activate([
+                            viewPinned.topAnchor.constraint(equalTo: timeView.bottomAnchor, constant: 5.0)
+                        ])
+                        if data.counter == "0" {
+                            NSLayoutConstraint.activate([
+                                viewPinned.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20)
+                            ])
+                        } else {
+                            NSLayoutConstraint.activate([
+                                viewPinned.trailingAnchor.constraint(equalTo: viewCounter.leadingAnchor, constant: -5)
+                            ])
+                        }
+                    }
                 } else {
                     titleView.centerYAnchor.constraint(equalTo: content.centerYAnchor).isActive = true
                     titleView.text = data.groupName
@@ -1439,6 +1654,20 @@ extension ContactChatViewController {
                         viewCounter.trailingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: -5).isActive = true
                         viewCounter.centerYAnchor.constraint(equalTo: content.centerYAnchor).isActive = true
                     }
+                    if data.pinned != 0 && !data.isFolPinned {
+                        NSLayoutConstraint.activate([
+                            viewPinned.centerYAnchor.constraint(equalTo: content.centerYAnchor)
+                        ])
+                        if data.counter == "0" {
+                            NSLayoutConstraint.activate([
+                                viewPinned.trailingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: -5)
+                            ])
+                        } else {
+                            NSLayoutConstraint.activate([
+                                viewPinned.trailingAnchor.constraint(equalTo: viewCounter.leadingAnchor, constant: -5)
+                            ])
+                        }
+                    }
                 }
             }
         case 1:
@@ -1658,12 +1887,16 @@ extension ContactChatViewController {
     
     override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
         let fontSize = Int(SecureUserDefaults.shared.value(forKey: "font_size") ?? "0")
+        var finalHeight = 75.0
+        if self.archivedChats.count > 0 && indexPath.row == 0 {
+            finalHeight = 50.0
+        }
         if fontSize == 4 {
-            return 85.0
+            finalHeight += 10
         } else if fontSize == 6 {
-            return 95.0
+            finalHeight += 20
         }
-        return 75.0
+        return finalHeight
     }
     
     private func getRealStatus(messageId: String) -> String {

+ 3 - 2
NexilisLite/NexilisLite/Source/View/Control/GroupDetailViewController.swift

@@ -281,9 +281,10 @@ class GroupDetailViewController: UITableViewController, UITextFieldDelegate {
                                                 lastName: cursorMember.string(forColumnIndex: 2) ?? "",
                                                 thumb: cursorMember.string(forColumnIndex: 3) ?? "",
                                                 position: cursorMember.string(forColumnIndex: 4) ?? "")
-                                if let cursorUser = Database.shared.getRecords(fmdb: fmdb, query: "SELECT user_type, official_account FROM BUDDY where f_pin='\(member.pin)'"), cursorUser.next() {
+                                if let cursorUser = Database.shared.getRecords(fmdb: fmdb, query: "SELECT user_type, official_account, image_id FROM BUDDY where f_pin='\(member.pin)'"), cursorUser.next() {
                                     member.userType = cursorUser.string(forColumnIndex: 0)
                                     member.official = cursorUser.string(forColumnIndex: 1)
+                                    member.thumb = cursorUser.string(forColumnIndex: 2) ?? ""
                                     cursorUser.close()
                                 }
                                 group.members.append(member)
@@ -612,7 +613,7 @@ class GroupDetailViewController: UITableViewController, UITextFieldDelegate {
                                 if result, let index = g.members.firstIndex(of: member) {
                                     DispatchQueue.main.async {
                                         g.members.remove(at: index)
-                                        tableView.deleteRows(at: [indexPath], with: .automatic)
+                                        tableView.deleteRows(at: [indexPath], with: .right)
                                         tableView.reloadData()
                                     }
                                 }

+ 1 - 2
NexilisLite/NexilisLite/Source/View/Control/SettingTableViewController.swift

@@ -668,8 +668,7 @@ public class SettingTableViewController: UITableViewController, UIGestureRecogni
                                     if Nexilis.showFB {
                                         Nexilis.floatingButton.removeFromSuperview()
                                         Nexilis.floatingButton = FloatingButton()
-                                        let viewController = (UIApplication.shared.windows.first?.rootViewController)!
-                                        Nexilis.addFB(viewController: viewController, fromMAB: true)
+                                        Nexilis.addFB()
                                     }
                                     var dataImage: [AnyHashable : Any] = [:]
                                     dataImage["name"] = ""

+ 3 - 6
NexilisLite/NexilisLite/Source/View/Control/SignUpSignIn.swift

@@ -168,8 +168,7 @@ public class SignUpSignIn: UIViewController {
                                     if Nexilis.showFB {
                                         Nexilis.floatingButton.removeFromSuperview()
                                         Nexilis.floatingButton = FloatingButton()
-                                        let viewController = (UIApplication.shared.windows.first?.rootViewController)!
-                                        Nexilis.addFB(viewController: viewController, fromMAB: true)
+                                        Nexilis.addFB()
                                     }
                                     if self.fromChangeNamePass{
                                         var vc = self.navigationController?.presentingViewController
@@ -333,8 +332,7 @@ public class SignUpSignIn: UIViewController {
                                     if Nexilis.showFB {
                                         Nexilis.floatingButton.removeFromSuperview()
                                         Nexilis.floatingButton = FloatingButton()
-                                        let viewController = (UIApplication.shared.windows.first?.rootViewController)!
-                                        Nexilis.addFB(viewController: viewController, fromMAB: true)
+                                        Nexilis.addFB()
                                     }
                                     NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
                                     if self.fromChangeNamePass{
@@ -375,8 +373,7 @@ public class SignUpSignIn: UIViewController {
                                     if Nexilis.showFB {
                                         Nexilis.floatingButton.removeFromSuperview()
                                         Nexilis.floatingButton = FloatingButton()
-                                        let viewController = (UIApplication.shared.windows.first?.rootViewController)!
-                                        Nexilis.addFB(viewController: viewController, fromMAB: true)
+                                        Nexilis.addFB()
                                     }
                                     NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
                                     if self.fromChangeNamePass{