Эх сурвалжийг харах

Merge remote-tracking branch 'origin/master'

kevin 2 жил өмнө
parent
commit
c4675a6930
22 өөрчлөгдсөн 1353 нэмэгдсэн , 587 устгасан
  1. 1 1
      appbuilder-ios/AppBuilder/AppBuilder/FirstTabViewController.swift
  2. 23 2
      appbuilder-ios/AppBuilder/AppBuilder/FourthTabViewController.swift
  3. 152 115
      appbuilder-ios/AppBuilder/AppBuilder/SecondTabViewController.swift
  4. 1 1
      appbuilder-ios/AppBuilder/AppBuilder/ThirdTabViewController.swift
  5. BIN
      appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Frameworks/WebPKit.framework/Frameworks/libwebp.a
  6. BIN
      appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Frameworks/WebPKit.framework/WebPKit
  7. BIN
      appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Info.plist
  8. BIN
      appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Modules/nuSDKService.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo
  9. 66 66
      appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Modules/nuSDKService.swiftmodule/arm64-apple-ios.abi.json
  10. BIN
      appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Modules/nuSDKService.swiftmodule/arm64-apple-ios.swiftmodule
  11. BIN
      appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/nuSDKService
  12. 6 2
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Palio.storyboard
  13. 6 3
      appbuilder-ios/NexilisLite/NexilisLite/Resource/id.lproj/Localizable.strings
  14. 11 3
      appbuilder-ios/NexilisLite/NexilisLite/Source/Extension.swift
  15. 10 3
      appbuilder-ios/NexilisLite/NexilisLite/Source/InquiryThread.swift
  16. 26 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/Model/NotifSound.swift
  17. 27 1
      appbuilder-ios/NexilisLite/NexilisLite/Source/Nexilis.swift
  18. 342 91
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift
  19. 427 177
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift
  20. 144 121
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ContactChatViewController.swift
  21. 1 1
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/GroupDetailViewController.swift
  22. 110 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/NotificationSound.swift

+ 1 - 1
appbuilder-ios/AppBuilder/AppBuilder/FirstTabViewController.swift

@@ -210,7 +210,7 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
             }
             if ViewController.checkIsChangePerson() {
                 if param2 == "like" {
-                    self.webView.evaluateJavaScript("likeProduct('\(param1)',true);")
+                    self.webView.evaluateJavaScript("likeProduct('\(param1)',1,true);")
                 } else if param2 == "comment" {
                     self.webView.evaluateJavaScript("openComment('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
                 } else if param2 == "report_user" {

+ 23 - 2
appbuilder-ios/AppBuilder/AppBuilder/FourthTabViewController.swift

@@ -35,6 +35,10 @@ public class FourthTabViewController: UIViewController, UITableViewDelegate, UIT
 //        tableView.separatorColor = .gray
         tableView.separatorStyle = .none
         
+        if PrefsUtil.getCpaasMode() == PrefsUtil.CPAAS_MODE_DOCKED {
+            tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 80, right: 0)
+        }
+        
         switchVibrateMode.tintColor = .gray
         switchSaveToGallery.tintColor = .gray
         switchAutoDownload.tintColor = .gray
@@ -241,8 +245,8 @@ public class FourthTabViewController: UIViewController, UITableViewDelegate, UIT
         })
         
         Item.menus["Call"] = [
-//            Item(icon: UIImage(systemName: "message"), title: "Incoming Message(s)".localized()),
-//            Item(icon: UIImage(systemName: "phone"), title: "Incoming Call(s)".localized()),
+            Item(icon: UIImage(systemName: "message"), title: "Notification Message(s)".localized()),
+            Item(icon: UIImage(systemName: "message"), title: "Notification Message(s) Group".localized()),
             Item(icon: UIImage(systemName: "iphone.homebutton.radiowaves.left.and.right"), title: "Vibrate Mode".localized()),
             Item(icon: UIImage(systemName: "photo.on.rectangle.angled"), title: "Save to Gallery".localized()),
             Item(icon: UIImage(systemName: "arrow.down.square"), title: "Auto Download".localized()),
@@ -330,6 +334,10 @@ public class FourthTabViewController: UIViewController, UITableViewDelegate, UIT
                 cell.accessoryType = .disclosureIndicator
             case "Login".localized():
                 cell.accessoryType = .disclosureIndicator
+            case "Notification Message(s)".localized():
+                cell.accessoryType = .disclosureIndicator
+            case "Notification Message(s) Group".localized():
+                cell.accessoryType = .disclosureIndicator
 //            case "Logout".localized():
             case "Change Admin / Internal Password".localized():
                 cell.accessoryType = .disclosureIndicator
@@ -558,6 +566,19 @@ public class FourthTabViewController: UIViewController, UITableViewDelegate, UIT
             navigationController.view.backgroundColor = .mainColor
             navigationController.modalPresentationStyle = .custom
             self.present(navigationController, animated: true)
+        } else if item.title == "Notification Message(s)".localized() || item.title == "Notification Message(s) Group".localized() {
+            let controller = NotificationSound()
+            if item.title != "Notification Message(s)".localized() {
+                controller.isPersonal = false
+            }
+            let navigationController = UINavigationController(rootViewController: controller)
+            navigationController.navigationBar.tintColor = .white
+            navigationController.navigationBar.barTintColor = .mainColor
+            navigationController.navigationBar.isTranslucent = false
+            let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
+            navigationController.navigationBar.titleTextAttributes = textAttributes
+            navigationController.view.backgroundColor = .mainColor
+            self.present(navigationController, animated: true)
         }
     }
     

+ 152 - 115
appbuilder-ios/AppBuilder/AppBuilder/SecondTabViewController.swift

@@ -119,6 +119,10 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
         tableView.tableHeaderView = segment
         tableView.tableFooterView = UIView()
         
+        if PrefsUtil.getCpaasMode() == PrefsUtil.CPAAS_MODE_DOCKED {
+            tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 80, right: 0)
+        }
+        
         pullBuddy()
         let cpaasMode = PrefsUtil.getCpaasMode()
         let isBurger = cpaasMode == PrefsUtil.CPAAS_MODE_BURGER
@@ -440,12 +444,13 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
     
     private func getGroupRecursive(fmdb: FMDatabase, id: String = "", parent: String = "") -> [Group] {
         var data: [Group] = []
-        var query = "select g.group_id, g.f_name, g.image_id, g.quote, g.created_by, g.created_date, g.parent, g.group_type, g.is_open, g.official, g.is_education from GROUPZ g where "
+        var query = "select g.group_id, g.f_name, g.image_id, g.quote, g.created_by, g.created_date, g.parent, g.group_type, g.is_open, g.official, g.is_education, g.level from GROUPZ g where "
         if id.isEmpty {
             query += "g.parent = '\(parent)'"
         } else {
             query += "g.group_id = '\(id)'"
         }
+        query += "order by 10 desc"
         if let cursor = Database.shared.getRecords(fmdb: fmdb, query: query) {
             while cursor.next() {
                 let group = Group(
@@ -460,10 +465,11 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
                     groupType: cursor.string(forColumnIndex: 7) ?? "",
                     isOpen: cursor.string(forColumnIndex: 8) ?? "",
                     official: cursor.string(forColumnIndex: 9) ?? "",
-                    isEducation: cursor.string(forColumnIndex: 10) ?? "")
+                    isEducation: cursor.string(forColumnIndex: 10) ?? "",
+                    level: cursor.string(forColumnIndex: 11) ?? "")
                 
                 if group.chatId.isEmpty {
-                    let lounge = Group(id: group.id, name: "Lounge".localized(), profile: "", quote: group.quote, by: group.by, date: group.date, parent: group.id, chatId: group.chatId, groupType: group.groupType, isOpen: group.isOpen, official: group.official, isEducation: group.isEducation, isLounge: true)
+                    let lounge = Group(id: group.id, name: "Lounge".localized(), profile: "", quote: group.quote, by: group.by, date: group.date, parent: group.id, chatId: group.chatId, groupType: group.groupType, isOpen: group.isOpen, official: group.official, isEducation: group.isEducation, isLounge: true, level: group.level != "-1" ? group.level : "2")
                     group.childs.append(lounge)
                 }
                 
@@ -480,22 +486,28 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
                                           groupType: group.groupType,
                                           isOpen: group.isOpen,
                                           official: group.official,
-                                          isEducation: group.isEducation)
+                                          isEducation: group.isEducation,
+                                          level: group.level != "-1" ? group.level : "2")
                         group.childs.append(topic)
                     }
                     topicCursor.close()
                 }
                 
                 if !group.id.isEmpty {
-                    if group.official == "1" {
-                        let idMe = UserDefaults.standard.string(forKey: "me") as String?
-                        if let cursorUser = Database.shared.getRecords(fmdb: fmdb, query: "SELECT user_type FROM BUDDY where f_pin='\(idMe!)'"), cursorUser.next() {
-                            group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
-                            cursorUser.close()
-                        }
-                    } else if group.official != "1"{
-                        group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
-                    }
+//                    if group.official == "1" {
+//                        let idMe = UserDefaults.standard.string(forKey: "me") as String?
+//                        if let cursorUser = Database.shared.getRecords(fmdb: fmdb, query: "SELECT user_type FROM BUDDY where f_pin='\(idMe!)'"), cursorUser.next() {
+//                            group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
+//                            cursorUser.close()
+//                        }
+//                    } else if group.official != "1"{
+//                        group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
+//                    }
+                    group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
+//                    group.childs = group.childs.sorted(by: { $0.name < $1.name })
+//                    let dataLounge = group.childs.filter({$0.name == "Lounge".localized()})
+//                    group.childs = group.childs.filter({ $0.name != "Lounge".localized() })
+//                    group.childs.insert(contentsOf: dataLounge, at: 0)
                 }
                 data.append(group)
             }
@@ -613,134 +625,154 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                 navigationController?.show(editorGroupVC, sender: nil)
             }
         case 1:
-            let group: Group
-            if isFilltering {
-                if indexPath.row == 0 {
-                    group = fillteredData[indexPath.section] as! Group
-                } else {
-                    if (fillteredData[indexPath.section] as! Group).childs.count > 0 {
-                        group = (fillteredData[indexPath.section] as! Group).childs[indexPath.row - 1]
-                    } else {
-                        return
-                    }
-                }
+            expandCollapseGroup(tableView: tableView, indexPath: indexPath)
+        default:
+            let data = contacts[indexPath.row]
+            let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
+            editorPersonalVC.hidesBottomBarWhenPushed = true
+            editorPersonalVC.unique_l_pin = data.pin
+            navigationController?.show(editorPersonalVC, sender: nil)
+        }
+    }
+    
+    func expandCollapseGroup(tableView: UITableView, indexPath: IndexPath) {
+        let group: Group
+        if isFilltering {
+            if indexPath.row == 0 {
+                group = fillteredData[indexPath.section] as! Group
             } else {
-                if indexPath.row == 0 {
-                    group = groups[indexPath.section]
+                if (fillteredData[indexPath.section] as! Group).childs.count > 0 {
+                    group = (fillteredData[indexPath.section] as! Group).childs[indexPath.row - 1]
                 } else {
-                    group = groups[indexPath.section].childs[indexPath.row - 1]
+                    return
                 }
             }
-            group.isSelected = !group.isSelected
-            if !group.isSelected{
-                var sects = 0
-                var sect = indexPath.section
-                var id = group.id
-                if let _ = groupMap[id] {
-                    var loooop = true
-                    repeat {
-                        let c = sect + 1
-                        if isFilltering {
-                            if let o = self.fillteredData[c] as? Group {
-                                if o.parent == id {
-                                    sects = sects + 1
-                                    sect = c
-                                    id = o.id
-                                    (self.fillteredData[c] as! Group).isSelected = false
-                                    self.groupMap.removeValue(forKey: (self.fillteredData[c] as! Group).id)
-                                }
-                                else {
-                                    loooop = false
-                                }
-                            }
-                        }
-                        else {
-                            if self.groups[c].parent == id {
+        } else {
+            if indexPath.row == 0 {
+                group = groups[indexPath.section]
+            } else {
+                group = groups[indexPath.section].childs[indexPath.row - 1]
+            }
+        }
+        group.isSelected = !group.isSelected
+        if !group.isSelected{
+            var sects = 0
+            var sect = indexPath.section
+            var id = group.id
+            if let _ = groupMap[id] {
+                var loooop = true
+                repeat {
+                    let c = sect + 1
+                    if isFilltering {
+                        if let o = self.fillteredData[c] as? Group {
+                            if o.parent == id {
                                 sects = sects + 1
                                 sect = c
-                                id = self.groups[c].id
-                                self.groups[c].isSelected = false
-                                self.groupMap.removeValue(forKey: self.groups[c].id)
+                                id = o.id
+                                (self.fillteredData[c] as! Group).isSelected = false
+                                self.groupMap.removeValue(forKey: (self.fillteredData[c] as! Group).id)
                             }
                             else {
                                 loooop = false
                             }
                         }
-                    } while(loooop)
-                }
-                for i in stride(from: sects, to: 0, by: -1){
-                    if isFilltering {
-                        self.fillteredData.remove(at: indexPath.section + i)
                     }
                     else {
-                        self.groups.remove(at: indexPath.section + i)
+                        if c < self.groups.count && self.groups[c].parent == id {
+                            sects = sects + 1
+                            sect = c
+                            id = self.groups[c].id
+                            self.groups[c].isSelected = false
+                            self.groupMap.removeValue(forKey: self.groups[c].id)
+                        }
+                        else {
+                            loooop = false
+                        }
                     }
+                } while(loooop)
+            }
+            for i in stride(from: sects, to: 0, by: -1){
+                if isFilltering {
+                    self.fillteredData.remove(at: indexPath.section + i)
+                }
+                else {
+                    self.groups.remove(at: indexPath.section + i)
                 }
-                groupMap.removeValue(forKey: group.id)
             }
-            if group.groupType == "NOTJOINED" {
-                let alert = UIAlertController(title: "Do you want to join this group?".localized(), message: "Groups : \(group.name)\nMembers: \(group.by)".localized(), preferredStyle: .alert)
-                alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
-                alert.addAction(UIAlertAction(title: "Join".localized(), style: .default, handler: {(_) in
-                    self.joinOpenGroup(groupId: group.id, completion: { result in
-                        if result {
-                            DispatchQueue.main.async {
-                                self.groupMap.removeAll()
-                                let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
-                                editorGroupVC.hidesBottomBarWhenPushed = true
-                                editorGroupVC.unique_l_pin = group.id
-                                self.navigationController?.show(editorGroupVC, sender: nil)
-                            }
+            groupMap.removeValue(forKey: group.id)
+        }
+        if group.groupType == "NOTJOINED" {
+            let alert = UIAlertController(title: "Do you want to join this group?".localized(), message: "Groups : \(group.name)\nMembers: \(group.by)".localized(), preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
+            alert.addAction(UIAlertAction(title: "Join".localized(), style: .default, handler: {(_) in
+                self.joinOpenGroup(groupId: group.id, completion: { result in
+                    if result {
+                        DispatchQueue.main.async {
+                            self.groupMap.removeAll()
+                            let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
+                            editorGroupVC.hidesBottomBarWhenPushed = true
+                            editorGroupVC.unique_l_pin = group.id
+                            self.navigationController?.show(editorGroupVC, sender: nil)
                         }
-                    })
-                }))
-                self.present(alert, animated: true, completion: nil)
-                return
-            }
-            if group.childs.count == 0 {
-                let groupId = group.chatId.isEmpty ? group.id : group.chatId
-                if let chooser = isChooser {
-                    chooser("4", groupId)
-                    dismiss(animated: true, completion: nil)
-                    return
+                    }
+                })
+            }))
+            self.present(alert, animated: true, completion: nil)
+            return
+        }
+        if group.childs.count == 0 {
+            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                let idMe = UserDefaults.standard.string(forKey: "me") as String?
+                if let cursorMember = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin from GROUPZ_MEMBER where group_id = '\(group.id)' and f_pin = '\(idMe!)'"), cursorMember.next() {
+                    let groupId = group.chatId.isEmpty ? group.id : group.chatId
+                    if let chooser = isChooser {
+                        chooser("4", groupId)
+                        dismiss(animated: true, completion: nil)
+                        return
+                    }
+                    self.groupMap.removeAll()
+                    let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
+                    editorGroupVC.hidesBottomBarWhenPushed = true
+                    editorGroupVC.unique_l_pin = groupId
+                    navigationController?.show(editorGroupVC, sender: nil)
+                    cursorMember.close()
+                } else {
+                    var viewController = UIApplication.shared.windows.first!.rootViewController
+                    if !(viewController is ViewController) {
+                        viewController = self.parent
+                    }
+                    if let viewController = viewController as? ViewController {
+                        viewController.view.makeToast("You are not a member of this group".localized(), duration: 0.5)
+                    }
                 }
-                self.groupMap.removeAll()
-                let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
-                editorGroupVC.hidesBottomBarWhenPushed = true
-                editorGroupVC.unique_l_pin = groupId
-                navigationController?.show(editorGroupVC, sender: nil)
+            })
+        } else {
+            if indexPath.row == 0 {
+                tableView.reloadData()
             } else {
-                if indexPath.row == 0 {
-                    tableView.reloadData()
-                } else {
-                    getGroups(id: group.id) { g in
-                        DispatchQueue.main.async {
-                            print("index path section: \(indexPath.section)")
-                            print("index path row: \(indexPath.row)")
+                getGroups(id: group.id) { g in
+                    DispatchQueue.main.async {
+                        print("index path section: \(indexPath.section)")
+                        print("index path row: \(indexPath.row)")
 //                            print("index path item: \(indexPath.item)")
-                            if self.isFilltering {
+                        if self.isFilltering {
 //                                self.fillteredData.remove(at: indexPath.section)
-                                if self.fillteredData[indexPath.section] is Group {
-                                    self.groupMap[(self.fillteredData[indexPath.section] as! Group).id] = 1
-                                    self.fillteredData.insert(contentsOf: g, at: indexPath.section + 1)
-                                }
-                            } else {
-//                                self.groups.remove(at: indexPath.section)
-                                self.groupMap[self.groups[indexPath.section].id] = 1
-                                self.groups.insert(contentsOf: g, at: indexPath.section + 1)
+                            if self.fillteredData[indexPath.section] is Group {
+                                self.groupMap[(self.fillteredData[indexPath.section] as! Group).id] = 1
+                                self.fillteredData.insert(contentsOf: g, at: indexPath.section + 1)
                             }
-                            print("groupMap: \(self.groupMap)")
-                            tableView.reloadData()
+                        } else {
+//                                self.groups.remove(at: indexPath.section)
+                            self.groupMap[self.groups[indexPath.section].id] = 1
+                            self.groups.insert(contentsOf: g, at: indexPath.section + 1)
                         }
+                        print("groupMap: \(self.groupMap)")
+                        tableView.reloadData()
+                        
+                        self.expandCollapseGroup(tableView: tableView, indexPath: IndexPath(row: 0, section: indexPath.section + 1))
                     }
                 }
             }
-        default:
-            let data = contacts[indexPath.row]
-            let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
-            editorPersonalVC.hidesBottomBarWhenPushed = true
-            editorPersonalVC.unique_l_pin = data.pin
-            navigationController?.show(editorPersonalVC, sender: nil)
         }
     }
     
@@ -1009,6 +1041,11 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                 content.image = image
             }
             cell.contentConfiguration = content
+            if group.level != "-1" && Int(group.level)! < 7 {
+                cell.contentView.layoutMargins = .init(top: 0.0, left: CGFloat(25 * Int(group.level)!), bottom: 0.0, right: 0)
+            } else if Int(group.level)! > 6 {
+                cell.contentView.layoutMargins = .init(top: 0.0, left: CGFloat(25 * (Int(group.level)! - 6)), bottom: 0.0, right: 0)
+            }
         default:
             cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifierContact", for: indexPath)
             var content = cell.defaultContentConfiguration()

+ 1 - 1
appbuilder-ios/AppBuilder/AppBuilder/ThirdTabViewController.swift

@@ -223,7 +223,7 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
             }
             if ViewController.checkIsChangePerson() {
                 if param2 == "like" {
-                    self.webView.evaluateJavaScript("likeProduct('\(param1)',true);")
+                    self.webView.evaluateJavaScript("likeProduct('\(param1)',1,true);")
                 } else if param2 == "comment" {
                     self.webView.evaluateJavaScript("openComment('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
                 } else if param2 == "report_user" {

BIN
appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Frameworks/WebPKit.framework/Frameworks/libwebp.a


BIN
appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Frameworks/WebPKit.framework/WebPKit


BIN
appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Info.plist


BIN
appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Modules/nuSDKService.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo


+ 66 - 66
appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Modules/nuSDKService.swiftmodule/arm64-apple-ios.abi.json

@@ -4232,14 +4232,14 @@
       "kind": "StringLiteral",
       "offset": 562,
       "length": 32,
-      "value": "\"nuSDK-S5-221002i-168-14.0-14.0\""
+      "value": "\"nuSDK-S5-221002o-170-14.0-14.0\""
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
       "offset": 728,
       "length": 3,
-      "value": "168"
+      "value": "170"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
@@ -4489,252 +4489,252 @@
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 14571,
+      "offset": 14624,
       "length": 1,
       "value": "0"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 16399,
+      "offset": 16452,
       "length": 1,
       "value": "0"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 17286,
+      "offset": 17339,
       "length": 1,
       "value": "0"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32203,
+      "offset": 32256,
       "length": 4,
       "value": "999"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32214,
+      "offset": 32267,
       "length": 19,
       "value": "998"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32240,
+      "offset": 32293,
       "length": 10,
       "value": "997"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32257,
+      "offset": 32310,
       "length": 10,
       "value": "996"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32274,
+      "offset": 32327,
       "length": 10,
       "value": "995"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32291,
+      "offset": 32344,
       "length": 17,
       "value": "994"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32315,
+      "offset": 32368,
       "length": 11,
       "value": "993"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32333,
+      "offset": 32386,
       "length": 9,
       "value": "992"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32349,
+      "offset": 32402,
       "length": 10,
       "value": "991"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32203,
+      "offset": 32256,
       "length": 1,
       "value": "999"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32214,
+      "offset": 32267,
       "length": 19,
       "value": "998"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32240,
+      "offset": 32293,
       "length": 10,
       "value": "997"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32257,
+      "offset": 32310,
       "length": 10,
       "value": "996"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32274,
+      "offset": 32327,
       "length": 10,
       "value": "995"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32291,
+      "offset": 32344,
       "length": 17,
       "value": "994"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32315,
+      "offset": 32368,
       "length": 11,
       "value": "993"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32333,
+      "offset": 32386,
       "length": 9,
       "value": "992"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32349,
+      "offset": 32402,
       "length": 10,
       "value": "991"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32203,
+      "offset": 32256,
       "length": 1,
       "value": "999"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32214,
+      "offset": 32267,
       "length": 19,
       "value": "998"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32240,
+      "offset": 32293,
       "length": 10,
       "value": "997"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32257,
+      "offset": 32310,
       "length": 10,
       "value": "996"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32274,
+      "offset": 32327,
       "length": 10,
       "value": "995"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32291,
+      "offset": 32344,
       "length": 17,
       "value": "994"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32315,
+      "offset": 32368,
       "length": 11,
       "value": "993"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32333,
+      "offset": 32386,
       "length": 9,
       "value": "992"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 32349,
+      "offset": 32402,
       "length": 10,
       "value": "991"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 33013,
+      "offset": 33066,
       "length": 4,
       "value": "999"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 33024,
+      "offset": 33077,
       "length": 10,
       "value": "998"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 33013,
+      "offset": 33066,
       "length": 1,
       "value": "999"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 33024,
+      "offset": 33077,
       "length": 10,
       "value": "998"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 33013,
+      "offset": 33066,
       "length": 1,
       "value": "999"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/API.swift",
       "kind": "IntegerLiteral",
-      "offset": 33024,
+      "offset": 33077,
       "length": 10,
       "value": "998"
     },
@@ -4834,194 +4834,194 @@
       "kind": "IntegerLiteral",
       "offset": 303,
       "length": 13,
-      "value": "1672788815836"
+      "value": "1698711494526"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 575,
+      "offset": 577,
       "length": 4,
       "value": "8192"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 750,
+      "offset": 752,
       "length": 2,
       "value": "32"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 860,
+      "offset": 862,
       "length": 1,
       "value": "0"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 890,
+      "offset": 892,
       "length": 1,
       "value": "1"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 920,
+      "offset": 922,
       "length": 1,
       "value": "2"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 950,
+      "offset": 952,
       "length": 1,
       "value": "3"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 1005,
+      "offset": 1007,
       "length": 1,
       "value": "5"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 1068,
+      "offset": 1070,
       "length": 1,
       "value": "7"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 1098,
+      "offset": 1100,
       "length": 1,
       "value": "8"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 1128,
+      "offset": 1130,
       "length": 1,
       "value": "9"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 1237,
+      "offset": 1239,
       "length": 2,
       "value": "14"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 1282,
+      "offset": 1284,
       "length": 2,
       "value": "15"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 1326,
+      "offset": 1328,
       "length": 2,
       "value": "20"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "StringLiteral",
-      "offset": 1460,
+      "offset": 1462,
       "length": 7,
       "value": "\"\t\""
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "StringLiteral",
-      "offset": 1696,
+      "offset": 1698,
       "length": 16,
       "value": "\"dqASyncMsgLock\""
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "StringLiteral",
-      "offset": 1831,
+      "offset": 1833,
       "length": 9,
       "value": "\"~<UNK>~\""
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 1909,
+      "offset": 1911,
       "length": 1,
       "value": "0"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 1957,
+      "offset": 1959,
       "length": 1,
       "value": "0"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 1993,
+      "offset": 1995,
       "length": 1,
       "value": "2"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 1997,
+      "offset": 1999,
       "length": 2,
       "value": "60"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 2002,
+      "offset": 2004,
       "length": 4,
       "value": "1000"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "StringLiteral",
-      "offset": 2194,
+      "offset": 2196,
       "length": 15,
       "value": "\"dqSyncMsgLock\""
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 2288,
+      "offset": 2290,
       "length": 1,
       "value": "0"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 2505,
+      "offset": 2507,
       "length": 4,
       "value": "666"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "StringLiteral",
-      "offset": 3164,
+      "offset": 3166,
       "length": 12,
       "value": "\"UserIDTest\""
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "BooleanLiteral",
-      "offset": 3207,
+      "offset": 3209,
       "length": 5,
       "value": "false"
     },
     {
       "filePath": "\/Users\/w.yudoaji\/ImagiNation\/XCode\/nuSDKService\/nuSDKService\/ConnectionNode.swift",
       "kind": "IntegerLiteral",
-      "offset": 3487,
+      "offset": 3489,
       "length": 1,
       "value": "0"
     },

BIN
appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/Modules/nuSDKService.swiftmodule/arm64-apple-ios.swiftmodule


BIN
appbuilder-ios/NexilisLite/Frameworks/nuSDKService.framework/nuSDKService


+ 6 - 2
appbuilder-ios/NexilisLite/NexilisLite/Resource/Palio.storyboard

@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
     <device id="retina6_1" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
         <capability name="Image references" minToolsVersion="12.0"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -2587,12 +2587,14 @@
                         <outlet property="buttonSendPhoto" destination="3ef-CD-2Pj" id="fzG-Y0-H3l"/>
                         <outlet property="buttonSendSticker" destination="Y6s-g6-F6l" id="3Ar-0n-M14"/>
                         <outlet property="constraintBottomAttachment" destination="tbE-PI-oI9" id="zwJ-JT-Tbq"/>
+                        <outlet property="constraintBottomTableViewWithTextfield" destination="6PR-yi-4lY" id="hia-Wx-6hv"/>
                         <outlet property="constraintLeftTextField" destination="jx0-o5-Ja0" id="1hP-FQ-qYY"/>
                         <outlet property="constraintTopTextField" destination="43A-hi-qkj" id="UCq-4N-QAW"/>
                         <outlet property="constraintViewTextField" destination="kBx-ys-A1s" id="A9C-IX-cZ2"/>
                         <outlet property="heightTextFieldSend" destination="BM5-gS-VIN" id="Ioh-Xm-Kd0"/>
                         <outlet property="tableChatView" destination="tSE-6b-2zD" id="qeC-k9-MJz"/>
                         <outlet property="textFieldSend" destination="Zwh-BN-IXY" id="Y3X-Y9-Tg4"/>
+                        <outlet property="viewAttachment" destination="UMZ-Cg-DEH" id="oNr-Nt-Yjk"/>
                         <outlet property="viewButton" destination="Xda-XC-yEA" id="SfN-gB-RKH"/>
                         <outlet property="viewTextfield" destination="yLm-Hp-3ZU" id="oLC-X1-VKP"/>
                     </connections>
@@ -2750,11 +2752,13 @@
                         <outlet property="buttonSendPhoto" destination="acF-7N-m3T" id="amJ-hi-fyj"/>
                         <outlet property="buttonSendSticker" destination="w8i-CY-6go" id="oZz-WV-6bq"/>
                         <outlet property="constraintBottomAttachment" destination="7cj-we-UtI" id="z0B-86-TwT"/>
+                        <outlet property="constraintBottomTableViewWithTextfield" destination="LOl-kg-Tex" id="aqm-eZ-phZ"/>
                         <outlet property="constraintTopTextField" destination="rwD-sI-rSc" id="d82-qJ-lee"/>
                         <outlet property="constraintViewTextField" destination="bT4-KC-g5c" id="KVz-UY-FuX"/>
                         <outlet property="heightTextFieldSend" destination="8Zi-nS-Q7O" id="V0u-tl-hcY"/>
                         <outlet property="tableChatView" destination="pVj-YY-BAA" id="0Nc-wF-dmw"/>
                         <outlet property="textFieldSend" destination="bW3-fS-6Qv" id="NWj-px-tv0"/>
+                        <outlet property="viewAttachment" destination="ZXb-nM-tPm" id="Bbu-bM-wLZ"/>
                         <outlet property="viewButton" destination="rs3-vX-pDB" id="4yF-Hx-qVU"/>
                         <outlet property="viewTextfield" destination="fnQ-OG-1Lg" id="KTx-21-3J7"/>
                     </connections>

+ 6 - 3
appbuilder-ios/NexilisLite/NexilisLite/Resource/id.lproj/Localizable.strings

@@ -1,4 +1,4 @@
-/* 
+/*
   Localizable.string.strings
   QmeraLite
 
@@ -76,8 +76,8 @@
 "Change Admin / Internal Password" = "Ubah Sandi Admin / Internal";
 "Change Device" = "Ubah Perangkat";
 "Change Language" = "Ubah Bahasa";
-"Incoming Message(s)" = "Pesan Masuk";
-"Incoming Call(s)" = "Panggilan Masuk";
+"Notification Message(s)" = "Pesan Notifikasi";
+"Notification Message(s) Group" = "Grup Pesan Notifikasi";
 "Vibrate Mode" = "Mode Getar";
 "Save to Gallery" = "Simpan ke Galeri";
 "Auto Download" = "Unduh Otomatis";
@@ -187,3 +187,6 @@
 "Invalid OTP" = "OTP tidak valid";
 "Successfully add friend" = "Berhasil menambahkan teman";
 "Login to Web" = "Masuk ke Web";
+"of" = "dari";
+"matches" = "cocok";
+"Not found" = "Tidak ditemukan";

+ 11 - 3
appbuilder-ios/NexilisLite/NexilisLite/Source/Extension.swift

@@ -574,7 +574,7 @@ extension String {
         return length
     }
     
-    public func richText(isEditing: Bool = false, first: Int = 0, last: Int = 0) -> NSAttributedString {
+    public func richText(isEditing: Bool = false, first: Int = 0, last: Int = 0, isSearching: Bool = false, textSearch: String = "") -> NSAttributedString {
         let font = UIFont.systemFont(ofSize: 12)
         let textUTF8 = String(self.utf8)
         let finalText = NSMutableAttributedString(string: textUTF8, attributes: [NSAttributedString.Key.font: font])
@@ -718,6 +718,14 @@ extension String {
                     }
                 }
             }
+            if isSearching {
+                let attributedText = NSMutableAttributedString(string: finalText.string) // 1
+                let range = NSString(string: finalText.string).range(of: textSearch, options: .caseInsensitive) // 2
+                let highlightColor = UIColor.systemYellow // 3
+                let highlightedAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.backgroundColor: highlightColor] // 4
+                
+                finalText.addAttributes(highlightedAttributes, range: range) // 5
+            }
         }
         
         return finalText
@@ -804,7 +812,7 @@ extension Bundle {
 }
 
 //extension UIFont {
-//    
+//
 //    static func register(from url: URL) throws {
 //        guard let fontDataProvider = CGDataProvider(url: url as CFURL) else {
 //            throw fatalError("Could not create font data provider for \(url).")
@@ -815,7 +823,7 @@ extension Bundle {
 //            throw error!.takeUnretainedValue()
 //        }
 //    }
-//    
+//
 //}
 
 extension UIButton {

+ 10 - 3
appbuilder-ios/NexilisLite/NexilisLite/Source/InquiryThread.swift

@@ -23,14 +23,21 @@ class InquiryThread {
     
     init() {
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
-            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select status, message from INQUIRY") {
+            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select status, message, id from INQUIRY") {
                 while cursor.next() {
                     let status = cursor.int(forColumnIndex: 0)
                     if status == 1 {
                         continue
                     }
-                    if let message = cursor.string(forColumnIndex: 1) {
-                        addQueue(message: TMessage(data: message))
+                    if let cursorMessage = Database.shared.getRecords(fmdb: fmdb, query: "select message_id from MESSAGE where message_id = '\(cursor.string(forColumnIndex: 2)!)'") {
+                        if cursorMessage.next() {
+                            if let message = cursor.string(forColumnIndex: 1) {
+                                addQueue(message: TMessage(data: message))
+                            }
+                        } else {
+                            _ = Database.shared.deleteRecord(fmdb: fmdb, table: "INQUIRY", _where: "id = '\(cursor.string(forColumnIndex: 2)!)'")
+                        }
+                        cursorMessage.close()
                     }
                 }
                 cursor.close()

+ 26 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/Model/NotifSound.swift

@@ -0,0 +1,26 @@
+//
+//  NotifSound.swift
+//  NexilisLite
+//
+//  Created by Akhmad Al Qindi Irsyam on 28/11/22.
+//
+
+import Foundation
+
+public class NotifSound: Model {
+    public var id: Int
+    public var name: String
+    public var isSelected: Bool
+    public var description: String
+    
+    public init(id: Int, name: String, isSelected: Bool = false) {
+        self.id = id
+        self.name = name
+        self.isSelected = isSelected
+        self.description = ""
+    }
+    
+    public static func == (lhs: NotifSound, rhs: NotifSound) -> Bool {
+        return lhs.id == rhs.id
+    }
+}

+ 27 - 1
appbuilder-ios/NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -1676,6 +1676,7 @@ extension Nexilis: CallDelegate {
 }
 
 var previewItem : NSURL?
+var floating: FloatingNotificationBanner!
 
 extension Nexilis: MessageDelegate {
     public func onReceiveComment(message: TMessage) {
@@ -2756,7 +2757,10 @@ extension Nexilis: MessageDelegate {
                     subtitle.attributedText = text.richText()
                     subtitle.textColor = .white
                     
-                    let floating = FloatingNotificationBanner(customView: container)
+                    if floating != nil {
+                        floating.dismiss()
+                    }
+                    floating = FloatingNotificationBanner(customView: container)
                     floating.bannerHeight = 100.0
                     floating.transparency = 0.9
                     
@@ -2812,6 +2816,28 @@ extension Nexilis: MessageDelegate {
                     }
                     
                     floating.show(queuePosition: .front, bannerPosition: .top, queue: NotificationBannerQueue(maxBannersOnScreenSimultaneously: 1), on: nil, edgeInsets: UIEdgeInsets(top: 8.0, left: 8.0, bottom: 0, right: 8.0), cornerRadius: 8.0, shadowColor: .clear, shadowOpacity: .zero, shadowBlurRadius: .zero, shadowCornerRadius: .zero, shadowOffset: .zero, shadowEdgeInsets: nil)
+                    let vibrateMode = UserDefaults.standard.bool(forKey: "vibrateMode")
+                    var soundId = UserDefaults.standard.string(forKey: "notifSoundPersonal") ?? ""
+                    if message.getBody(key: CoreMessage_TMessageKey.MESSAGE_SCOPE_ID) == "4" {
+                        soundId = UserDefaults.standard.string(forKey: "notifSoundGroup") ?? ""
+                    }
+                    var systemSoundID: SystemSoundID!
+                    if soundId.isEmpty {
+                        systemSoundID = 1007
+                    } else {
+                        systemSoundID = SystemSoundID(Int(soundId.components(separatedBy: ":")[0])!)
+                    }
+                    if vibrateMode {
+                        AudioServicesPlaySystemSound(systemSoundID)
+                    } else {
+                        var nameSound = "sms-received1"
+                        if systemSoundID != 1007 {
+                            nameSound = soundId.components(separatedBy: ":")[1]
+                        }
+                        let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/\(nameSound).caf")
+                        AudioServicesCreateSystemSoundID(url as CFURL, &systemSoundID)
+                        AudioServicesPlaySystemSound(systemSoundID)
+                    }
                     if !onGoingCC.isEmpty {
                         floating.autoDismiss = false
                     }

+ 342 - 91
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -30,6 +30,8 @@ public class EditorGroup: UIViewController {
     @IBOutlet var constraintBottomAttachment: NSLayoutConstraint!
     @IBOutlet var viewTextfield: UIView!
     @IBOutlet weak var buttonAckConfidential: UIButton!
+    @IBOutlet weak var constraintBottomTableViewWithTextfield: NSLayoutConstraint!
+    @IBOutlet weak var viewAttachment: UIStackView!
     public var dataGroup: [String: Any?] = [:]
     public var dataTopic: [String: Any?] = [:]
     var dataMessages: [[String: Any?]] = []
@@ -56,6 +58,7 @@ public class EditorGroup: UIViewController {
     var copySession = false
     var forwardSession = false
     var deleteSession = false
+    var isSearching = false
     let containerMultpileSelectSession = UIView()
     let viewSticker = UIView()
     let containerLink = UIView()
@@ -64,6 +67,14 @@ public class EditorGroup: UIViewController {
     let containerAction = UIView()
     var allowTyping = true
     let contactChatNav = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "contactChatNav") as! UINavigationController
+    var searchBar: UISearchBar!
+    var constraintBottomContainerMultpileSelectSession = NSLayoutConstraint()
+    var titleSearchMatches: UILabel!
+    var textSearch = ""
+    var countMatchesSearch = 0
+    var lastScrollIdxSearch = 0
+    var buttonUp: UIButton!
+    var buttonDown: UIButton!
     
     public override func viewDidDisappear(_ animated: Bool) {
         if self.isMovingFromParent {
@@ -159,6 +170,8 @@ public class EditorGroup: UIViewController {
     }
     
     private func setRightButtonItem() {
+        navigationItem.rightBarButtonItems = nil
+        navigationItem.rightBarButtonItem = nil
         let menu = UIMenu(title: "", children: [
             UIAction(title: "Delete Conversation".localized(), handler: {(_) in
                 let alert = UIAlertController(title: "", message: "Are you sure to delete all message in this conversation?".localized(), preferredStyle: .alert)
@@ -169,8 +182,8 @@ public class EditorGroup: UIViewController {
                         if (self.dataTopic["chat_id"] as! String != "") {
                             l_pin = self.dataTopic["chat_id"] as! String
                         }
-                        _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", _where: "l_pin='\(l_pin)'")
                         _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE", _where: "(l_pin='\(self.dataGroup["group_id"]!!)' and chat_id='\(self.dataTopic["chat_id"]!!)') and message_scope_id='4'")
+                        _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", _where: "l_pin='\(l_pin)'")
                     })
                     if self.fromNotification {
                         self.didTapExit()
@@ -186,7 +199,27 @@ public class EditorGroup: UIViewController {
         ])
         if !isHistoryCC {
             let moreIcon = UIBarButtonItem(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular, scale: .default)), menu: menu)
-            navigationItem.rightBarButtonItem = moreIcon
+            let buttonSearch = UIBarButtonItem(image: UIImage(systemName: "magnifyingglass", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular, scale: .default)), style: .plain, target: self, action: #selector(search(sender:)))
+            navigationItem.rightBarButtonItems = [moreIcon,buttonSearch]
+        } else {
+            let buttonSearch = UIBarButtonItem(image: UIImage(systemName: "magnifyingglass", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular, scale: .default)), style: .plain, target: self, action: #selector(search(sender:)))
+            navigationItem.rightBarButtonItem = buttonSearch
+        }
+    }
+    
+    @objc func search(sender: UIBarButtonItem) {
+        self.isSearching = true
+        if self.reffId != nil {
+            self.deleteReplyView()
+        }
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
+            let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
+            if !self.isHistoryCC {
+                self.navigationItem.rightBarButtonItems = nil
+            }
+            self.navigationItem.rightBarButtonItem = cancelButton
+            self.changeAppBar()
+            self.addMultipleSelectSession()
         }
     }
     
@@ -466,37 +499,49 @@ public class EditorGroup: UIViewController {
         let viewAppBar = UIView()
         viewAppBar.frame.size = CGSize(width: self.view.frame.size.width, height: 44)
         
-        let imageProfile = UIImageView(frame: CGRect(x: 0, y: 7, width: 30, height: 30))
-        imageProfile.circle()
-        imageProfile.clipsToBounds = true
-        viewAppBar.addSubview(imageProfile)
-        let pictureImage = dataGroup["image_id"]!
-        if (pictureImage as! String != "" && pictureImage != nil) {
-            imageProfile.setImage(name: pictureImage! as! String)
-            imageProfile.contentMode = .scaleAspectFill
-        } else {
-            imageProfile.image = UIImage(systemName: "person.3")
-            imageProfile.contentMode = .scaleAspectFit
-            imageProfile.backgroundColor = .lightGray
-        }
-        
-        let titleNavigation = UILabel(frame: CGRect(x: 35, y: 0, width: viewAppBar.frame.size.width - 150, height: 44))
-        viewAppBar.addSubview(titleNavigation)
-        if (dataGroup["official"] as! String == "1") {
-            if !isHistoryCC {
-                titleNavigation.set(image: UIImage(named: "ic_official_flag", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(.white), with: "  \(dataGroup["f_name"]!!) (\(dataTopic["title"]!!))", size: 15, y: -4)
+        if !isSearching {
+            let imageProfile = UIImageView(frame: CGRect(x: 0, y: 7, width: 30, height: 30))
+            imageProfile.circle()
+            imageProfile.clipsToBounds = true
+            viewAppBar.addSubview(imageProfile)
+            let pictureImage = dataGroup["image_id"]!
+            if (pictureImage as! String != "" && pictureImage != nil) {
+                imageProfile.setImage(name: pictureImage! as! String)
+                imageProfile.contentMode = .scaleAspectFill
             } else {
-                titleNavigation.text = (dataGroup["f_name"] as? String)! + " " + "Contact Center".localized()
+                imageProfile.image = UIImage(systemName: "person.3")
+                imageProfile.contentMode = .scaleAspectFit
+                imageProfile.backgroundColor = .lightGray
             }
+            
+            let titleNavigation = UILabel(frame: CGRect(x: 35, y: 0, width: viewAppBar.frame.size.width - 150, height: 44))
+            viewAppBar.addSubview(titleNavigation)
+            if (dataGroup["official"] as! String == "1") {
+                if !isHistoryCC {
+                    titleNavigation.set(image: UIImage(named: "ic_official_flag", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(.white), with: "  \(dataGroup["f_name"]!!) (\(dataTopic["title"]!!))", size: 15, y: -4)
+                } else {
+                    titleNavigation.text = (dataGroup["f_name"] as? String)! + " " + "Contact Center".localized()
+                }
+            } else {
+                titleNavigation.text = (dataGroup["f_name"] as? String)! + " (\(dataTopic["title"]!!))"
+            }
+            titleNavigation.textColor = .white
+            titleNavigation.font = UIFont.systemFont(ofSize: 12).bold
+            
+            navigationItem.titleView = viewAppBar
         } else {
-            titleNavigation.text = (dataGroup["f_name"] as? String)! + " (\(dataTopic["title"]!!))"
-        }
-        titleNavigation.textColor = .white
-        titleNavigation.font = UIFont.systemFont(ofSize: 12).bold
-        
-        navigationItem.titleView = viewAppBar
-        
-        if copySession || forwardSession || deleteSession {
+            searchBar = UISearchBar()
+            searchBar.autocapitalizationType = .none
+            searchBar.delegate = self
+            searchBar.searchTextField.tintColor = .mainColor
+            searchBar.barTintColor = .secondaryColor
+            searchBar.searchTextField.backgroundColor = .secondaryColor
+            searchBar.showsCancelButton = false
+            navigationItem.titleView = searchBar
+            self.definesPresentationContext = true
+        }
+        
+        if copySession || forwardSession || deleteSession || isSearching {
             navigationItem.hidesBackButton = true
             navigationController?.interactivePopGestureRecognizer?.isEnabled = false
         } else {
@@ -1007,11 +1052,15 @@ public class EditorGroup: UIViewController {
     }
     
     @objc func dismissKeyboard() {
-        textFieldSend.resignFirstResponder() // dismiss keyoard
-        if (self.constraintBottomAttachment.constant != 0.0) {
-            constraintBottomAttachment.constant = 0.0
-            self.viewSticker.removeConstraints(self.viewSticker.constraints)
-            self.viewSticker.removeFromSuperview()
+        if isSearching {
+            searchBar.resignFirstResponder()
+        } else {
+            textFieldSend.resignFirstResponder() // dismiss keyoard
+            if (self.constraintBottomAttachment.constant != 0.0) {
+                constraintBottomAttachment.constant = 0.0
+                self.viewSticker.removeConstraints(self.viewSticker.constraints)
+                self.viewSticker.removeFromSuperview()
+            }
         }
     }
     
@@ -1031,13 +1080,21 @@ public class EditorGroup: UIViewController {
             
             if self.constraintViewTextField.constant != keyboardHeight - 60 {
                 self.constraintViewTextField.constant = keyboardHeight - 60
+                if isSearching {
+                    self.constraintViewTextField.constant = self.constraintViewTextField.constant + 60
+                    self.constraintBottomContainerMultpileSelectSession.constant = -keyboardHeight
+                }
                 UIView.animate(withDuration: TimeInterval(duration), animations: {
                     self.view.layoutIfNeeded()
                 })
-                if (self.currentIndexpath != nil) {
-                    self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: false)
-                } else {
+                if isSearching {
                     self.tableChatView.scrollToBottom()
+                } else {
+                    if (self.currentIndexpath != nil) {
+                        self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: false)
+                    } else {
+                        self.tableChatView.scrollToBottom()
+                    }
                 }
             }
         }
@@ -1049,6 +1106,7 @@ public class EditorGroup: UIViewController {
             let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
             
             self.constraintViewTextField.constant = 0
+            self.constraintBottomContainerMultpileSelectSession.constant = 0
             UIView.animate(withDuration: TimeInterval(duration), animations: {
                 self.view.layoutIfNeeded()
             })
@@ -1828,21 +1886,32 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
             if self.removed {
                 return
             }
-            self.handleReply(indexPath: indexPath!)
+            if self.isSearching {
+                self.cancelAction()
+            }
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
+                self.handleReply(indexPath: indexPath!)
+            })
         })
         let forward = UIAction(title: "Forward".localized(), image: UIImage(systemName: "arrowshape.turn.up.right.fill"), handler: {(_) in
             if self.removed {
                 return
             }
-            self.forwardSession = true
+            if self.isSearching {
+                self.cancelAction()
+            }
             if self.reffId != nil {
                 self.deleteReplyView()
             }
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
+                self.forwardSession = true
                 let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
+                if !self.isHistoryCC {
+                    self.navigationItem.rightBarButtonItems = nil
+                }
                 self.navigationItem.rightBarButtonItem = cancelButton
                 self.changeAppBar()
-                let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as! String == dataMessages[indexPath!.row]["message_id"] as! String})
+                let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == dataMessages[indexPath!.row]["message_id"] as? String})
                 if idx != nil{
                     self.dataMessages[idx!]["isSelected"] = true
                 }
@@ -1854,15 +1923,21 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
             if self.removed {
                 return
             }
-            self.copySession = true
+            if self.isSearching {
+                self.cancelAction()
+            }
             if self.reffId != nil {
                 self.deleteReplyView()
             }
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
+                self.copySession = true
                 let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
+                if !self.isHistoryCC {
+                    self.navigationItem.rightBarButtonItems = nil
+                }
                 self.navigationItem.rightBarButtonItem = cancelButton
                 self.changeAppBar()
-                let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as! String == dataMessages[indexPath!.row]["message_id"] as! String})
+                let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == dataMessages[indexPath!.row]["message_id"] as? String})
                 if idx != nil{
                     self.dataMessages[idx!]["isSelected"] = true
                 }
@@ -1884,15 +1959,21 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
             if self.removed {
                 return
             }
-            self.deleteSession = true
+            if self.isSearching {
+                self.cancelAction()
+            }
             if self.reffId != nil {
                 self.deleteReplyView()
             }
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
+                self.deleteSession = true
                 let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
+                if !self.isHistoryCC {
+                    self.navigationItem.rightBarButtonItems = nil
+                }
                 self.navigationItem.rightBarButtonItem = cancelButton
                 self.changeAppBar()
-                let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as! String == dataMessages[indexPath!.row]["message_id"] as! String})
+                let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == dataMessages[indexPath!.row]["message_id"] as? String})
                 if idx != nil{
                     self.dataMessages[idx!]["isSelected"] = true
                 }
@@ -1939,6 +2020,25 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
                 self.forwardSession = false
             } else if self.deleteSession {
                 self.deleteSession = false
+            } else if self.isSearching {
+                self.countMatchesSearch = 0
+                self.isSearching = false
+            }
+            if self.viewTextfield.isHidden {
+                self.viewTextfield.isHidden = false
+            }
+            if self.viewAttachment.isHidden {
+                self.viewAttachment.isHidden = false
+            }
+            if !self.isSearching {
+                self.constraintBottomTableViewWithTextfield.constant = self.constraintBottomTableViewWithTextfield.constant + 70
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
+                    if (self.currentIndexpath != nil) {
+                        self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: true)
+                    } else {
+                        self.tableChatView.scrollToBottom()
+                    }
+                })
             }
             let data = self.dataMessages.filter({ $0["isSelected"] as! Bool == true })
             for i in 0..<data.count {
@@ -1956,13 +2056,17 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
     }
     
     private func addMultipleSelectSession() {
+        viewTextfield.isHidden = true
+        viewAttachment.isHidden = true
+        constraintBottomTableViewWithTextfield.constant = constraintBottomTableViewWithTextfield.constant - 70
         view.addSubview(containerMultpileSelectSession)
         containerMultpileSelectSession.translatesAutoresizingMaskIntoConstraints = false
+        constraintBottomContainerMultpileSelectSession = containerMultpileSelectSession.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0)
         NSLayoutConstraint.activate([
             containerMultpileSelectSession.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
             containerMultpileSelectSession.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
-            containerMultpileSelectSession.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
-            containerMultpileSelectSession.heightAnchor.constraint(equalToConstant: 120)
+            constraintBottomContainerMultpileSelectSession,
+            containerMultpileSelectSession.heightAnchor.constraint(equalToConstant: 50)
         ])
         containerMultpileSelectSession.backgroundColor = .white
         addSubviewMultipleSession()
@@ -1984,52 +2088,104 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
         container.layer.shadowColor = UIColor.black.cgColor
         container.backgroundColor = .secondaryColor
         
-        let title = UILabel()
-        container.addSubview(title)
-        title.translatesAutoresizingMaskIntoConstraints = false
-        NSLayoutConstraint.activate([
-            title.centerXAnchor.constraint(equalTo: container.centerXAnchor),
-            title.centerYAnchor.constraint(equalTo:container.centerYAnchor),
-        ])
-        let countSelected = dataMessages.filter({ $0["isSelected"] as! Bool == true }).count
-        title.text = "\(countSelected) " + "Selected".localized()
-        title.textColor = .mainColor
-        title.font = UIFont.systemFont(ofSize: 15).bold
-        
-        let button = UIImageView()
-        container.addSubview(button)
-        button.translatesAutoresizingMaskIntoConstraints = false
-        NSLayoutConstraint.activate([
-            button.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 15),
-            button.centerYAnchor.constraint(equalTo:container.centerYAnchor),
-            button.widthAnchor.constraint(equalToConstant: 30),
-            button.heightAnchor.constraint(equalToConstant: 30),
-        ])
-        if copySession {
-            button.image = UIImage(systemName: "doc.on.doc")
-            if countSelected == 0{
-                button.tintColor = .gray
-            } else {
-                button.tintColor = .mainColor
-            }
-        } else if forwardSession {
-            button.image = UIImage(systemName: "arrowshape.turn.up.right")
-            if countSelected == 0{
-                button.tintColor = .gray
-            } else {
-                button.tintColor = .mainColor
-            }
-        } else if deleteSession {
-            button.image = UIImage(systemName: "trash")
-            if countSelected == 0{
-                button.tintColor = .gray
-            } else {
-                button.tintColor = .red
+        if !isSearching {
+            let title = UILabel()
+            container.addSubview(title)
+            title.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                title.centerXAnchor.constraint(equalTo: container.centerXAnchor),
+                title.centerYAnchor.constraint(equalTo:container.centerYAnchor),
+            ])
+            let countSelected = dataMessages.filter({ $0["isSelected"] as! Bool == true }).count
+            title.text = "\(countSelected) " + "Selected".localized()
+            title.textColor = .mainColor
+            title.font = UIFont.systemFont(ofSize: 15).bold
+            
+            let button = UIImageView()
+            container.addSubview(button)
+            button.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                button.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 15),
+                button.centerYAnchor.constraint(equalTo:container.centerYAnchor),
+                button.widthAnchor.constraint(equalToConstant: 30),
+                button.heightAnchor.constraint(equalToConstant: 30),
+            ])
+            if copySession {
+                button.image = UIImage(systemName: "doc.on.doc")
+                if countSelected == 0{
+                    button.tintColor = .gray
+                } else {
+                    button.tintColor = .mainColor
+                }
+            } else if forwardSession {
+                button.image = UIImage(systemName: "arrowshape.turn.up.right")
+                if countSelected == 0{
+                    button.tintColor = .gray
+                } else {
+                    button.tintColor = .mainColor
+                }
+            } else if deleteSession {
+                button.image = UIImage(systemName: "trash")
+                if countSelected == 0{
+                    button.tintColor = .gray
+                } else {
+                    button.tintColor = .red
+                }
             }
+            let buttonGesture = UITapGestureRecognizer(target: self, action: #selector(sessionAction))
+            button.isUserInteractionEnabled = true
+            button.addGestureRecognizer(buttonGesture)
+        } else {
+            buttonUp = UIButton()
+            container.addSubview(buttonUp)
+            buttonUp.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                buttonUp.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 10),
+                buttonUp.centerYAnchor.constraint(equalTo:container.centerYAnchor),
+                buttonUp.widthAnchor.constraint(equalToConstant: 30),
+                buttonUp.heightAnchor.constraint(equalToConstant: 30),
+            ])
+            buttonUp.addTarget(self, action: #selector(upSearchText), for: .touchUpInside)
+            
+            buttonDown = UIButton()
+            container.addSubview(buttonDown)
+            buttonDown.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                buttonDown.leadingAnchor.constraint(equalTo: buttonUp.trailingAnchor, constant: 15),
+                buttonDown.centerYAnchor.constraint(equalTo:container.centerYAnchor),
+                buttonDown.widthAnchor.constraint(equalToConstant: 30),
+                buttonDown.heightAnchor.constraint(equalToConstant: 30),
+            ])
+            buttonDown.addTarget(self, action: #selector(downSearchText), for: .touchUpInside)
+            
+            buttonUp.setImage(UIImage(systemName: "chevron.up"), for: .normal)
+            buttonUp.tintColor = .gray
+            
+            buttonDown.setImage(UIImage(systemName: "chevron.down"), for: .normal)
+            buttonDown.tintColor = .gray
+            
+            titleSearchMatches = UILabel()
+            container.addSubview(titleSearchMatches)
+            titleSearchMatches.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                titleSearchMatches.centerXAnchor.constraint(equalTo: container.centerXAnchor),
+                titleSearchMatches.centerYAnchor.constraint(equalTo:container.centerYAnchor),
+            ])
+            titleSearchMatches.textColor = .mainColor
+            titleSearchMatches.font = UIFont.systemFont(ofSize: 15.0).bold
+            titleSearchMatches.isHidden = true
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
+                self.searchBar.becomeFirstResponder()
+            })
         }
-        let buttonGesture = UITapGestureRecognizer(target: self, action: #selector(sessionAction))
-        button.isUserInteractionEnabled = true
-        button.addGestureRecognizer(buttonGesture)
+    }
+    
+    @objc func upSearchText() {
+        scrollToFirstSearchMessage(indexScroll: lastScrollIdxSearch + 1)
+    }
+    
+    @objc func downSearchText() {
+        scrollToFirstSearchMessage(indexScroll: lastScrollIdxSearch - 1)
     }
     
     @objc func sessionAction() {
@@ -2864,6 +3020,13 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
             }
         }
         
+        if isSearching && textSearch.count > 1 {
+            messageText.attributedText = textChat!.richText(isSearching: true, textSearch: textSearch)
+            if textChat!.lowercased().contains(textSearch) {
+                countMatchesSearch += 1
+            }
+        }
+        
         let stringDate = (dataMessages[indexPath.row]["server_date"] as! String)
         let date = Date(milliseconds: Int64(stringDate)!)
         let formatter = DateFormatter()
@@ -3700,4 +3863,92 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
             imageSticker.heightAnchor.constraint(equalToConstant: 30).isActive = true
         }
     }
+    
+    func scrollToFirstSearchMessage(indexScroll: Int = 1) {
+        if textSearch.count < 2 {
+            return
+        }
+        var lastIndex = 0
+        let messageTextForSearch: [[String: Any?]] = self.dataMessages.reversed()
+        for idx in 0..<messageTextForSearch.count {
+            if (messageTextForSearch[idx]["message_text"] as! String).lowercased().contains(textSearch) {
+                lastIndex += 1
+                if lastIndex < indexScroll {
+                    continue
+                }
+                lastScrollIdxSearch = lastIndex
+                let section = self.dataDates.firstIndex(of: messageTextForSearch[idx]["chat_date"] as! String)
+                if section == nil {
+                    return
+                }
+                let row = self.dataMessages.filter({ $0["chat_date"] as! String == self.dataDates[section!]}).firstIndex(where: { $0["message_id"] as! String == messageTextForSearch[idx]["message_id"] as! String})
+                if row == nil {
+                    return
+                }
+                let indexPath = IndexPath(row: row!, section: section!)
+                self.tableChatView.scrollToRow(at: indexPath, at: .middle, animated: true)
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+                    if let cell = self.tableChatView.cellForRow(at: indexPath) {
+                        let containerMessage = cell.contentView.subviews[1]
+                        let idMe = UserDefaults.standard.string(forKey: "me") as String?
+                        if (messageTextForSearch[idx]["f_pin"] as? String == idMe) {
+                            containerMessage.backgroundColor = .blueBubbleColor.withAlphaComponent(0.3)
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+                                if (messageTextForSearch[idx]["attachment_flag"] as? String == "11") {
+                                    containerMessage.backgroundColor = .clear
+                                } else {
+                                    containerMessage.backgroundColor = .blueBubbleColor
+                                }
+                            }
+                        } else {
+                            containerMessage.backgroundColor = .grayColor.withAlphaComponent(0.3)
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+                                if (messageTextForSearch[idx]["attachment_flag"] as? String == "11") {
+                                    containerMessage.backgroundColor = .clear
+                                } else {
+                                    containerMessage.backgroundColor = .grayColor
+                                }
+                            }
+                        }
+                    }
+                }
+                titleSearchMatches.isHidden = false
+                if countMatchesSearch != 0 {
+                    if countMatchesSearch > 1 {
+                        titleSearchMatches.text = "\(lastScrollIdxSearch) " + "of".localized() + " \(countMatchesSearch) " + "matches".localized()
+                    } else {
+                        titleSearchMatches.text = "\(countMatchesSearch) " + "matches".localized()
+                    }
+                } else {
+                    titleSearchMatches.text = "Not found".localized()
+                }
+                if lastScrollIdxSearch == countMatchesSearch || countMatchesSearch == 0 {
+                    buttonUp.isEnabled = false
+                    buttonUp.tintColor = .gray
+                } else {
+                    buttonUp.isEnabled = true
+                    buttonUp.tintColor = .mainColor
+                }
+                if countMatchesSearch == 0 || lastScrollIdxSearch == 1 || countMatchesSearch == 1 {
+                    buttonDown.isEnabled = false
+                    buttonDown.tintColor = .gray
+                } else {
+                    buttonDown.isEnabled = true
+                    buttonDown.tintColor = .mainColor
+                }
+                break
+            }
+        }
+    }
+}
+
+extension EditorGroup: UISearchBarDelegate {
+    
+    public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
+        textSearch = searchText
+        countMatchesSearch = 0
+        titleSearchMatches.isHidden = true
+        tableChatView.reloadData()
+        scrollToFirstSearchMessage()
+    }
 }

+ 427 - 177
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -31,6 +31,8 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     @IBOutlet var viewTextfield: UIView!
     @IBOutlet weak var buttonAckConfidential: UIButton!
     @IBOutlet weak var constraintLeftTextField: NSLayoutConstraint!
+    @IBOutlet weak var constraintBottomTableViewWithTextfield: NSLayoutConstraint!
+    @IBOutlet weak var viewAttachment: UIStackView!
     public var dataPerson: [String: String?] = [:]
     var dataMessages: [[String: Any?]] = []
     var dataDates: [String] = []
@@ -59,6 +61,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     var copySession = false
     var forwardSession = false
     var deleteSession = false
+    var isSearching = false
     let containerMultpileSelectSession = UIView()
     let containerAction = UIView()
     var removed = false
@@ -78,6 +81,14 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     var listTimerCredential: [String: Int] = [:]
     var timerCredential: [String: Timer] = [:]
     let contactChatNav = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "contactChatNav") as! UINavigationController
+    var searchBar: UISearchBar!
+    var constraintBottomContainerMultpileSelectSession = NSLayoutConstraint()
+    var titleSearchMatches: UILabel!
+    var textSearch = ""
+    var countMatchesSearch = 0
+    var lastScrollIdxSearch = 0
+    var buttonUp: UIButton!
+    var buttonDown: UIButton!
     
     public override func viewDidDisappear(_ animated: Bool) {
         if self.isMovingFromParent {
@@ -215,33 +226,29 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     
     private func setRightButtonItem() {
         navigationItem.rightBarButtonItems = nil
-        var menu = UIMenu(title: "", children: [
-            UIAction(title: "Delete Conversation".localized(), handler: {(_) in
-                if !self.isContactCenter {
-                    let alert = UIAlertController(title: "", message: "Are you sure to delete all message in this conversation?".localized(), preferredStyle: .alert)
-                    alert.addAction(UIAlertAction(title: "Cancel".localized(), style: UIAlertAction.Style.default, handler: nil))
-                    alert.addAction(UIAlertAction(title: "Delete".localized(), style: .destructive, handler: {(_) in
-                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                            _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", _where: "l_pin='\(self.dataPerson["f_pin"]!!)'")
-                            _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE", _where: "(f_pin='\(self.dataPerson["f_pin"]!!)' or l_pin='\(self.dataPerson["f_pin"]!!)') and (message_scope_id='3' or message_scope_id='18') and is_call_center = 0")
-                        })
-                        if self.fromNotification {
-                            self.didTapExit()
-                        } else {
-                            self.navigationController?.popViewController(animated: true)
-                        }
-//                        self.dataMessages.removeAll()
-//                        self.dataDates.removeAll()
-//                        self.tableChatView.reloadData()
-                    }))
-                    self.present(alert, animated: true, completion: nil)
-                }
-            })
-        ])
+        var menu = UIMenu()
         let exblock = User.getData(pin: self.dataPerson["f_pin"]!!)?.ex_block
         blocking = exblock == nil ? "0" : exblock!.isEmpty ? "0" : exblock!
         if blocking == "1" && self.dataPerson["f_pin"]!! != "-999" {
             menu = UIMenu(title: "", children: [
+                UIAction(title: "Search".localized(), handler: {(_) in
+                    self.isSearching = true
+                    if self.reffId != nil {
+                        self.deleteReplyView()
+                    }
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
+                        let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
+                        if self.dataPerson["f_pin"] != "-999" && !self.isContactCenter {
+                            self.navigationItem.rightBarButtonItems = nil
+                        }
+                        self.navigationItem.rightBarButtonItem = cancelButton
+                        if self.isContactCenter || self.fromNotification {
+                            self.navigationItem.leftBarButtonItem = nil
+                        }
+                        self.changeAppBar()
+                        self.addMultipleSelectSession()
+                    }
+                }),
                 UIAction(title: "Unblock".localized(), handler: {(_) in
                     if !self.isContactCenter {
                         DispatchQueue.global().async {
@@ -282,13 +289,11 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                         let alert = UIAlertController(title: "", message: "Are you sure to delete all message in this conversation?".localized(), preferredStyle: .alert)
                         alert.addAction(UIAlertAction(title: "Cancel".localized(), style: UIAlertAction.Style.default, handler: nil))
                         alert.addAction(UIAlertAction(title: "Delete".localized(), style: .destructive, handler: {(_) in
-                            DispatchQueue.global().async {
-                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                                    _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", _where: "l_pin='\(self.dataPerson["f_pin"]!!)'")
-                                    _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE", _where: "(f_pin='\(self.dataPerson["f_pin"]!!)' or l_pin='\(self.dataPerson["f_pin"]!!)') and (message_scope_id='3' or message_scope_id='18') and is_call_center = 0")
-                                })
-                                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
-                            }
+                            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE", _where: "(f_pin='\(self.dataPerson["f_pin"]!!)' or l_pin='\(self.dataPerson["f_pin"]!!)') and (message_scope_id='3' or message_scope_id='18') and is_call_center = 0")
+                                _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", _where: "l_pin='\(self.dataPerson["f_pin"]!!)'")
+                            })
+                            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
                             if self.fromNotification {
                                 self.didTapExit()
                             } else {
@@ -306,6 +311,24 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         } else if blocking == "0" {
             if self.dataPerson["f_pin"]!! != "-999"{
                 menu = UIMenu(title: "", children: [
+                    UIAction(title: "Search".localized(), handler: {(_) in
+                        self.isSearching = true
+                        if self.reffId != nil {
+                            self.deleteReplyView()
+                        }
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
+                            let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
+                            if self.dataPerson["f_pin"] != "-999" && !self.isContactCenter {
+                                self.navigationItem.rightBarButtonItems = nil
+                            }
+                            self.navigationItem.rightBarButtonItem = cancelButton
+                            if self.isContactCenter || self.fromNotification {
+                                self.navigationItem.leftBarButtonItem = nil
+                            }
+                            self.changeAppBar()
+                            self.addMultipleSelectSession()
+                        }
+                    }),
                     UIAction(title: "Block".localized(), handler: {(_) in
                         if !self.isContactCenter {
                             DispatchQueue.global().async {
@@ -366,6 +389,24 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                 ])
             } else {
                 menu = UIMenu(title: "", children: [
+                    UIAction(title: "Search".localized(), handler: {(_) in
+                        self.isSearching = true
+                        if self.reffId != nil {
+                            self.deleteReplyView()
+                        }
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
+                            let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
+                            if self.dataPerson["f_pin"] != "-999" && !self.isContactCenter {
+                                self.navigationItem.rightBarButtonItems = nil
+                            }
+                            self.navigationItem.rightBarButtonItem = cancelButton
+                            if self.isContactCenter || self.fromNotification {
+                                self.navigationItem.leftBarButtonItem = nil
+                            }
+                            self.changeAppBar()
+                            self.addMultipleSelectSession()
+                        }
+                    }),
                     UIAction(title: "Delete Conversation".localized(), handler: {(_) in
                         if !self.isContactCenter {
                             let alert = UIAlertController(title: "", message: "Are you sure to delete all message in this conversation?".localized(), preferredStyle: .alert)
@@ -575,116 +616,127 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         let viewAppBar = UIView()
         viewAppBar.frame.size = CGSize(width: self.view.frame.size.width, height: 44)
         
-        let imageProfile = UIImageView(frame: CGRect(x: 0, y: 7, width: 30, height: 30))
-        imageProfile.circle()
-        imageProfile.clipsToBounds = true
-        let pictureImage = dataPerson["picture"]!
-        var count = 0
-        if isContactCenter {
-            if fPinContacCenter.isEmpty && isRequestContactCenter {
-                getImage(name: dataPerson["picture"]!!, placeholderImage: UIImage(systemName: "person.circle.fill")!) { result, isDownloaded, image in
-                    imageProfile.image = image
-                }
-                viewAppBar.addSubview(imageProfile)
-            } else {
-                if users.count == 1 {
-                    viewAppBar.addSubview(imageProfile)
-                    getImage(name: users[0].thumb, placeholderImage: UIImage(systemName: "person.circle.fill")!) { result, isDownloaded, image in
+        if !isSearching {
+            let imageProfile = UIImageView(frame: CGRect(x: 0, y: 7, width: 30, height: 30))
+            imageProfile.circle()
+            imageProfile.clipsToBounds = true
+            let pictureImage = dataPerson["picture"]!
+            var count = 0
+            if isContactCenter {
+                if fPinContacCenter.isEmpty && isRequestContactCenter {
+                    getImage(name: dataPerson["picture"]!!, placeholderImage: UIImage(systemName: "person.circle.fill")!) { result, isDownloaded, image in
                         imageProfile.image = image
-                        imageProfile.contentMode = .scaleAspectFit
                     }
+                    viewAppBar.addSubview(imageProfile)
                 } else {
-                    for user in users {
-                        if count == 3 {
-                            count += 1
-                            continue
+                    if users.count == 1 {
+                        viewAppBar.addSubview(imageProfile)
+                        getImage(name: users[0].thumb, placeholderImage: UIImage(systemName: "person.circle.fill")!) { result, isDownloaded, image in
+                            imageProfile.image = image
+                            imageProfile.contentMode = .scaleAspectFit
                         }
-                        if count == 0 {
-                            let pictures = UIImageView(frame: CGRect(x: 0, y: 7, width: 30, height: 30))
-                            pictures.circle()
-                            pictures.clipsToBounds = true
-                            viewAppBar.addSubview(pictures)
-                            getImage(name: user.thumb, placeholderImage: UIImage(systemName: "person.circle.fill")!) { result, isDownloaded, image in
-                                pictures.image = image
-                                pictures.contentMode = .scaleAspectFit
+                    } else {
+                        for user in users {
+                            if count == 3 {
+                                count += 1
+                                continue
                             }
-                        } else {
-                            let pictures = UIImageView(frame: CGRect(x: count * 20 , y: 7, width: 30, height: 30))
-                            pictures.circle()
-                            pictures.clipsToBounds = true
-                            viewAppBar.addSubview(pictures)
-                            getImage(name: user.thumb, placeholderImage: UIImage(systemName: "person.circle.fill")!) { result, isDownloaded, image in
-                                pictures.image = image
-                                pictures.contentMode = .scaleAspectFit
+                            if count == 0 {
+                                let pictures = UIImageView(frame: CGRect(x: 0, y: 7, width: 30, height: 30))
+                                pictures.circle()
+                                pictures.clipsToBounds = true
+                                viewAppBar.addSubview(pictures)
+                                getImage(name: user.thumb, placeholderImage: UIImage(systemName: "person.circle.fill")!) { result, isDownloaded, image in
+                                    pictures.image = image
+                                    pictures.contentMode = .scaleAspectFit
+                                }
+                            } else {
+                                let pictures = UIImageView(frame: CGRect(x: count * 20 , y: 7, width: 30, height: 30))
+                                pictures.circle()
+                                pictures.clipsToBounds = true
+                                viewAppBar.addSubview(pictures)
+                                getImage(name: user.thumb, placeholderImage: UIImage(systemName: "person.circle.fill")!) { result, isDownloaded, image in
+                                    pictures.image = image
+                                    pictures.contentMode = .scaleAspectFit
+                                }
                             }
+                            count += 1
                         }
-                        count += 1
                     }
                 }
-            }
-        } else if dataPerson["f_pin"]!! == "-999" {
-            viewAppBar.addSubview(imageProfile)
-            if Utils.getIconDock() != nil {
-                let dataImage = try? Data(contentsOf: URL(string: Utils.getUrlDock()!)!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
-                if dataImage != nil {
-                    imageProfile.image = UIImage(data: dataImage!)
+            } else if dataPerson["f_pin"]!! == "-999" {
+                viewAppBar.addSubview(imageProfile)
+                if Utils.getIconDock() != nil {
+                    let dataImage = try? Data(contentsOf: URL(string: Utils.getUrlDock()!)!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
+                    if dataImage != nil {
+                        imageProfile.image = UIImage(data: dataImage!)
+                    }
+                } else {
+                    imageProfile.image = UIImage(named: "pb_button", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
                 }
-            } else {
-                imageProfile.image = UIImage(named: "pb_button", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+                imageProfile.contentMode = .scaleAspectFit
             }
-            imageProfile.contentMode = .scaleAspectFit
-        }
-        else if (pictureImage != "" && pictureImage != nil) {
-            viewAppBar.addSubview(imageProfile)
-            imageProfile.setImage(name: pictureImage!)
-            imageProfile.contentMode = .scaleAspectFill
-        } else {
-            viewAppBar.addSubview(imageProfile)
-            imageProfile.image = UIImage(systemName: "person")
-            imageProfile.contentMode = .scaleAspectFit
-            imageProfile.backgroundColor = .lightGray
-        }
-        
-        var titleNavigation = UILabel(frame: CGRect(x: 35, y: 0, width: viewAppBar.frame.size.width - 150, height: 44))
-        if dataPerson["f_pin"]!! != "-999"  && !isContactCenter && (blocking == "-1" || blocking == "1") {
-            titleNavigation = UILabel(frame: CGRect(x: 35, y: 0, width: viewAppBar.frame.size.width - 250, height: 44))
-        } else if isContactCenter {
-            if users.count > 0 {
-                titleNavigation = UILabel(frame: CGRect(x: 35 * (CGFloat(users.count)) - (CGFloat((users.count - 1) * 15)), y: 0, width: viewAppBar.frame.size.width - 150 - (35 * (CGFloat(users.count - 1)) - (CGFloat((users.count - 1) * 15))), height: 44))
+            else if (pictureImage != "" && pictureImage != nil) {
+                viewAppBar.addSubview(imageProfile)
+                imageProfile.setImage(name: pictureImage!)
+                imageProfile.contentMode = .scaleAspectFill
+            } else {
+                viewAppBar.addSubview(imageProfile)
+                imageProfile.image = UIImage(systemName: "person")
+                imageProfile.contentMode = .scaleAspectFit
+                imageProfile.backgroundColor = .lightGray
             }
-        }
-        viewAppBar.addSubview(titleNavigation)
-        if ((dataPerson["isOfficial"] == "1" && !isContactCenter) || (dataPerson["isOfficial"] == "1" && fPinContacCenter.isEmpty)) {
-            var name = dataPerson["name"]!!
-            if (isContactCenter) {
-                name = name + " " + "Contact Center".localized()
+            
+            var titleNavigation = UILabel(frame: CGRect(x: 35, y: 0, width: viewAppBar.frame.size.width - 150, height: 44))
+            if dataPerson["f_pin"]!! != "-999"  && !isContactCenter && (blocking == "-1" || blocking == "1") {
+                titleNavigation = UILabel(frame: CGRect(x: 35, y: 0, width: viewAppBar.frame.size.width - 250, height: 44))
+            } else if isContactCenter {
+                if users.count > 0 {
+                    titleNavigation = UILabel(frame: CGRect(x: 35 * (CGFloat(users.count)) - (CGFloat((users.count - 1) * 15)), y: 0, width: viewAppBar.frame.size.width - 150 - (35 * (CGFloat(users.count - 1)) - (CGFloat((users.count - 1) * 15))), height: 44))
+                }
             }
-            titleNavigation.set(image: UIImage(named: "ic_official_flag", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(.white), with: "  \(name)", size: 15, y: -4)
-        } else {
-            if !isContactCenter {
-                titleNavigation.text = dataPerson["name"] as? String
+            viewAppBar.addSubview(titleNavigation)
+            if ((dataPerson["isOfficial"] == "1" && !isContactCenter) || (dataPerson["isOfficial"] == "1" && fPinContacCenter.isEmpty)) {
+                var name = dataPerson["name"]!!
+                if (isContactCenter) {
+                    name = name + " " + "Contact Center".localized()
+                }
+                titleNavigation.set(image: UIImage(named: "ic_official_flag", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(.white), with: "  \(name)", size: 15, y: -4)
             } else {
-                if users.count == 1 {
-                    titleNavigation.text = users[0].fullName
+                if !isContactCenter {
+                    titleNavigation.text = dataPerson["name"] as? String
                 } else {
-                    var stringName = ""
-                    for user in users {
-                        if stringName.isEmpty {
-                            stringName = user.fullName
-                        } else {
-                            stringName += ", \(user.fullName)"
+                    if users.count == 1 {
+                        titleNavigation.text = users[0].fullName
+                    } else {
+                        var stringName = ""
+                        for user in users {
+                            if stringName.isEmpty {
+                                stringName = user.fullName
+                            } else {
+                                stringName += ", \(user.fullName)"
+                            }
                         }
+                        titleNavigation.text = stringName
                     }
-                    titleNavigation.text = stringName
                 }
             }
+            titleNavigation.textColor = .white
+            titleNavigation.font = UIFont.systemFont(ofSize: 12).bold
+            navigationItem.titleView = viewAppBar
+        } else {
+            searchBar = UISearchBar()
+            searchBar.autocapitalizationType = .none
+            searchBar.delegate = self
+            searchBar.searchTextField.tintColor = .mainColor
+            searchBar.barTintColor = .secondaryColor
+            searchBar.searchTextField.backgroundColor = .secondaryColor
+            searchBar.showsCancelButton = false
+            navigationItem.titleView = searchBar
+            self.definesPresentationContext = true
         }
-        titleNavigation.textColor = .white
-        titleNavigation.font = UIFont.systemFont(ofSize: 12).bold
-        
-        navigationItem.titleView = viewAppBar
         
-        if copySession || forwardSession || deleteSession {
+        if copySession || forwardSession || deleteSession || isSearching {
             navigationItem.hidesBackButton = true
             navigationController?.interactivePopGestureRecognizer?.isEnabled = false
         } else {
@@ -1748,11 +1800,15 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     }
     
     @objc func dismissKeyboard() {
-        textFieldSend.resignFirstResponder() // dismiss keyoard
-        if (self.constraintBottomAttachment.constant != 0.0) {
-            constraintBottomAttachment.constant = 0.0
-            self.viewSticker.removeConstraints(self.viewSticker.constraints)
-            self.viewSticker.removeFromSuperview()
+        if isSearching {
+            searchBar.resignFirstResponder()
+        } else {
+            textFieldSend.resignFirstResponder() // dismiss keyoard
+            if (self.constraintBottomAttachment.constant != 0.0) {
+                constraintBottomAttachment.constant = 0.0
+                self.viewSticker.removeConstraints(self.viewSticker.constraints)
+                self.viewSticker.removeFromSuperview()
+            }
         }
     }
     
@@ -1825,6 +1881,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
             
             self.constraintViewTextField.constant = 0
+            self.constraintBottomContainerMultpileSelectSession.constant = 0
             UIView.animate(withDuration: TimeInterval(duration), animations: {
                 self.view.layoutIfNeeded()
             })
@@ -1847,13 +1904,21 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             
             if self.constraintViewTextField.constant != keyboardHeight - 60 {
                 self.constraintViewTextField.constant = keyboardHeight - 60
+                if isSearching {
+                    self.constraintViewTextField.constant = self.constraintViewTextField.constant + 60
+                    self.constraintBottomContainerMultpileSelectSession.constant = -keyboardHeight
+                }
                 UIView.animate(withDuration: TimeInterval(duration), animations: {
                     self.view.layoutIfNeeded()
                 })
-                if (self.currentIndexpath != nil) {
-                    self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: false)
-                } else {
+                if isSearching {
                     self.tableChatView.scrollToBottom()
+                } else {
+                    if (self.currentIndexpath != nil) {
+                        self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: false)
+                    } else {
+                        self.tableChatView.scrollToBottom()
+                    }
                 }
             }
         }
@@ -2864,17 +2929,25 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
             if self.removed {
                 return
             }
-            self.handleReply(indexPath: indexPath!)
+            if self.isSearching {
+                self.cancelAction()
+            }
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
+                self.handleReply(indexPath: indexPath!)
+            })
         })
         let forward = UIAction(title: "Forward".localized(), image: UIImage(systemName: "arrowshape.turn.up.right.fill"), handler: {(_) in
             if self.removed {
                 return
             }
-            self.forwardSession = true
+            if self.isSearching {
+                self.cancelAction()
+            }
             if self.reffId != nil {
                 self.deleteReplyView()
             }
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
+                self.forwardSession = true
                 let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
                 if self.dataPerson["f_pin"] != "-999" && !self.isContactCenter {
                     self.navigationItem.rightBarButtonItems = nil
@@ -2896,11 +2969,14 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
             if self.removed {
                 return
             }
-            self.copySession = true
+            if self.isSearching {
+                self.cancelAction()
+            }
             if self.reffId != nil {
                 self.deleteReplyView()
             }
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
+                self.copySession = true
                 let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
                 if self.dataPerson["f_pin"] != "-999" && !self.isContactCenter {
                     self.navigationItem.rightBarButtonItems = nil
@@ -2931,11 +3007,14 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
             if self.removed {
                 return
             }
-            self.deleteSession = true
+            if self.isSearching {
+                self.cancelAction()
+            }
             if self.reffId != nil {
                 self.deleteReplyView()
             }
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
+                self.deleteSession = true
                 let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
                 if self.dataPerson["f_pin"] != "-999" && !self.isContactCenter {
                     self.navigationItem.rightBarButtonItems = nil
@@ -2995,6 +3074,25 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
                 self.forwardSession = false
             } else if self.deleteSession {
                 self.deleteSession = false
+            } else if self.isSearching {
+                self.countMatchesSearch = 0
+                self.isSearching = false
+            }
+            if self.viewTextfield.isHidden {
+                self.viewTextfield.isHidden = false
+            }
+            if self.viewAttachment.isHidden {
+                self.viewAttachment.isHidden = false
+            }
+            if !self.isSearching {
+                self.constraintBottomTableViewWithTextfield.constant = self.constraintBottomTableViewWithTextfield.constant + 70
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
+                    if (self.currentIndexpath != nil) {
+                        self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: true)
+                    } else {
+                        self.tableChatView.scrollToBottom()
+                    }
+                })
             }
             let data = self.dataMessages.filter({ $0["isSelected"] as! Bool == true })
             for i in 0..<data.count {
@@ -3016,13 +3114,17 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
     }
     
     private func addMultipleSelectSession() {
+        viewTextfield.isHidden = true
+        viewAttachment.isHidden = true
+        constraintBottomTableViewWithTextfield.constant = constraintBottomTableViewWithTextfield.constant - 70
         view.addSubview(containerMultpileSelectSession)
         containerMultpileSelectSession.translatesAutoresizingMaskIntoConstraints = false
+        constraintBottomContainerMultpileSelectSession = containerMultpileSelectSession.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0)
         NSLayoutConstraint.activate([
             containerMultpileSelectSession.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
             containerMultpileSelectSession.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
-            containerMultpileSelectSession.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
-            containerMultpileSelectSession.heightAnchor.constraint(equalToConstant: 120)
+            constraintBottomContainerMultpileSelectSession,
+            containerMultpileSelectSession.heightAnchor.constraint(equalToConstant: 50)
         ])
         containerMultpileSelectSession.backgroundColor = .white
         addSubviewMultipleSession()
@@ -3044,52 +3146,104 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
         container.layer.shadowColor = UIColor.black.cgColor
         container.backgroundColor = .secondaryColor
         
-        let title = UILabel()
-        container.addSubview(title)
-        title.translatesAutoresizingMaskIntoConstraints = false
-        NSLayoutConstraint.activate([
-            title.centerXAnchor.constraint(equalTo: container.centerXAnchor),
-            title.centerYAnchor.constraint(equalTo:container.centerYAnchor),
-        ])
-        let countSelected = dataMessages.filter({ $0["isSelected"] as! Bool == true }).count
-        title.text = "\(countSelected) " + "Selected".localized()
-        title.textColor = .mainColor
-        title.font = UIFont.systemFont(ofSize: 15.0).bold
-        
-        let button = UIImageView()
-        container.addSubview(button)
-        button.translatesAutoresizingMaskIntoConstraints = false
-        NSLayoutConstraint.activate([
-            button.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 15),
-            button.centerYAnchor.constraint(equalTo:container.centerYAnchor),
-            button.widthAnchor.constraint(equalToConstant: 30),
-            button.heightAnchor.constraint(equalToConstant: 30),
-        ])
-        if copySession {
-            button.image = UIImage(systemName: "doc.on.doc")
-            if countSelected == 0 {
-                button.tintColor = .gray
-            } else {
-                button.tintColor = .mainColor
-            }
-        } else if forwardSession {
-            button.image = UIImage(systemName: "arrowshape.turn.up.right")
-            if countSelected == 0 {
-                button.tintColor = .gray
-            } else {
-                button.tintColor = .mainColor
-            }
-        } else if deleteSession {
-            button.image = UIImage(systemName: "trash")
-            if countSelected == 0 {
-                button.tintColor = .gray
-            } else {
-                button.tintColor = .red
+        if !isSearching {
+            let title = UILabel()
+            container.addSubview(title)
+            title.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                title.centerXAnchor.constraint(equalTo: container.centerXAnchor),
+                title.centerYAnchor.constraint(equalTo:container.centerYAnchor),
+            ])
+            let countSelected = dataMessages.filter({ $0["isSelected"] as! Bool == true }).count
+            title.text = "\(countSelected) " + "Selected".localized()
+            title.textColor = .mainColor
+            title.font = UIFont.systemFont(ofSize: 15.0).bold
+            
+            let button = UIImageView()
+            container.addSubview(button)
+            button.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                button.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 15),
+                button.centerYAnchor.constraint(equalTo:container.centerYAnchor),
+                button.widthAnchor.constraint(equalToConstant: 30),
+                button.heightAnchor.constraint(equalToConstant: 30),
+            ])
+            if copySession {
+                button.image = UIImage(systemName: "doc.on.doc")
+                if countSelected == 0 {
+                    button.tintColor = .gray
+                } else {
+                    button.tintColor = .mainColor
+                }
+            } else if forwardSession {
+                button.image = UIImage(systemName: "arrowshape.turn.up.right")
+                if countSelected == 0 {
+                    button.tintColor = .gray
+                } else {
+                    button.tintColor = .mainColor
+                }
+            } else if deleteSession {
+                button.image = UIImage(systemName: "trash")
+                if countSelected == 0 {
+                    button.tintColor = .gray
+                } else {
+                    button.tintColor = .red
+                }
             }
+            let buttonGesture = UITapGestureRecognizer(target: self, action: #selector(sessionAction))
+            button.isUserInteractionEnabled = true
+            button.addGestureRecognizer(buttonGesture)
+        } else {
+            buttonUp = UIButton()
+            container.addSubview(buttonUp)
+            buttonUp.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                buttonUp.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 10),
+                buttonUp.centerYAnchor.constraint(equalTo:container.centerYAnchor),
+                buttonUp.widthAnchor.constraint(equalToConstant: 30),
+                buttonUp.heightAnchor.constraint(equalToConstant: 30),
+            ])
+            buttonUp.addTarget(self, action: #selector(upSearchText), for: .touchUpInside)
+            
+            buttonDown = UIButton()
+            container.addSubview(buttonDown)
+            buttonDown.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                buttonDown.leadingAnchor.constraint(equalTo: buttonUp.trailingAnchor, constant: 15),
+                buttonDown.centerYAnchor.constraint(equalTo:container.centerYAnchor),
+                buttonDown.widthAnchor.constraint(equalToConstant: 30),
+                buttonDown.heightAnchor.constraint(equalToConstant: 30),
+            ])
+            buttonDown.addTarget(self, action: #selector(downSearchText), for: .touchUpInside)
+            
+            buttonUp.setImage(UIImage(systemName: "chevron.up"), for: .normal)
+            buttonUp.tintColor = .gray
+            
+            buttonDown.setImage(UIImage(systemName: "chevron.down"), for: .normal)
+            buttonDown.tintColor = .gray
+            
+            titleSearchMatches = UILabel()
+            container.addSubview(titleSearchMatches)
+            titleSearchMatches.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                titleSearchMatches.centerXAnchor.constraint(equalTo: container.centerXAnchor),
+                titleSearchMatches.centerYAnchor.constraint(equalTo:container.centerYAnchor),
+            ])
+            titleSearchMatches.textColor = .mainColor
+            titleSearchMatches.font = UIFont.systemFont(ofSize: 15.0).bold
+            titleSearchMatches.isHidden = true
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
+                self.searchBar.becomeFirstResponder()
+            })
         }
-        let buttonGesture = UITapGestureRecognizer(target: self, action: #selector(sessionAction))
-        button.isUserInteractionEnabled = true
-        button.addGestureRecognizer(buttonGesture)
+    }
+    
+    @objc func upSearchText() {
+        scrollToFirstSearchMessage(indexScroll: lastScrollIdxSearch + 1)
+    }
+    
+    @objc func downSearchText() {
+        scrollToFirstSearchMessage(indexScroll: lastScrollIdxSearch - 1)
     }
     
     @objc func sessionAction() {
@@ -4133,6 +4287,13 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
             }
         }
         
+        if isSearching && textSearch.count > 1 {
+            messageText.attributedText = textChat.richText(isSearching: true, textSearch: textSearch)
+            if textChat.lowercased().contains(textSearch) {
+                countMatchesSearch += 1
+            }
+        }
+        
         let stringDate = (dataMessages[indexPath.row]["server_date"] as? String) ?? ""
         if !stringDate.isEmpty {
             if (dataMessages[indexPath.row]["credential"] as? String) == "1" && dataMessages[indexPath.row]["lock"] as? String != "2" {
@@ -5035,6 +5196,83 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
             imageSticker.heightAnchor.constraint(equalToConstant: 30).isActive = true
         }
     }
+    
+    func scrollToFirstSearchMessage(indexScroll: Int = 1) {
+        if textSearch.count < 2 {
+            return
+        }
+        var lastIndex = 0
+        let messageTextForSearch: [[String: Any?]] = self.dataMessages.reversed()
+        for idx in 0..<messageTextForSearch.count {
+            if (messageTextForSearch[idx]["message_text"] as! String).lowercased().contains(textSearch) {
+                lastIndex += 1
+                if lastIndex < indexScroll {
+                    continue
+                }
+                lastScrollIdxSearch = lastIndex
+                let section = self.dataDates.firstIndex(of: messageTextForSearch[idx]["chat_date"] as! String)
+                if section == nil {
+                    return
+                }
+                let row = self.dataMessages.filter({ $0["chat_date"] as! String == self.dataDates[section!]}).firstIndex(where: { $0["message_id"] as? String == messageTextForSearch[idx]["message_id"] as? String})
+                if row == nil {
+                    return
+                }
+                let indexPath = IndexPath(row: row!, section: section!)
+                self.tableChatView.scrollToRow(at: indexPath, at: .middle, animated: true)
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+                    if let cell = self.tableChatView.cellForRow(at: indexPath) {
+                        let containerMessage = cell.contentView.subviews[0]
+                        let idMe = UserDefaults.standard.string(forKey: "me") as String?
+                        if (messageTextForSearch[idx]["f_pin"] as? String == idMe) {
+                            containerMessage.backgroundColor = .blueBubbleColor.withAlphaComponent(0.3)
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+                                if (messageTextForSearch[idx]["attachment_flag"] as? String == "11") {
+                                    containerMessage.backgroundColor = .clear
+                                } else {
+                                    containerMessage.backgroundColor = .blueBubbleColor
+                                }
+                            }
+                        } else {
+                            containerMessage.backgroundColor = .grayColor.withAlphaComponent(0.3)
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+                                if (messageTextForSearch[idx]["attachment_flag"] as? String == "11") {
+                                    containerMessage.backgroundColor = .clear
+                                } else {
+                                    containerMessage.backgroundColor = .grayColor
+                                }
+                            }
+                        }
+                    }
+                }
+                titleSearchMatches.isHidden = false
+                if countMatchesSearch != 0 {
+                    if countMatchesSearch > 1 {
+                        titleSearchMatches.text = "\(lastScrollIdxSearch) " + "of".localized() + " \(countMatchesSearch) " + "matches".localized()
+                    } else {
+                        titleSearchMatches.text = "\(countMatchesSearch) " + "matches".localized()
+                    }
+                } else {
+                    titleSearchMatches.text = "Not found".localized()
+                }
+                if lastScrollIdxSearch == countMatchesSearch || countMatchesSearch == 0 {
+                    buttonUp.isEnabled = false
+                    buttonUp.tintColor = .gray
+                } else {
+                    buttonUp.isEnabled = true
+                    buttonUp.tintColor = .mainColor
+                }
+                if countMatchesSearch == 0 || lastScrollIdxSearch == 1 || countMatchesSearch == 1 {
+                    buttonDown.isEnabled = false
+                    buttonDown.tintColor = .gray
+                } else {
+                    buttonDown.isEnabled = true
+                    buttonDown.tintColor = .mainColor
+                }
+                break
+            }
+        }
+    }
 }
 
 extension UITableView {
@@ -5079,6 +5317,18 @@ extension UIImage {
     }
 }
 
+extension EditorPersonal: UISearchBarDelegate {
+    
+    public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
+        textSearch = searchText
+        countMatchesSearch = 0
+        titleSearchMatches.isHidden = true
+        tableChatView.reloadData()
+        scrollToFirstSearchMessage()
+    }
+}
+
+
 public class ObjectGesture: UITapGestureRecognizer {
     public var message_id = ""
     public var image_id = ""

+ 144 - 121
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ContactChatViewController.swift

@@ -294,12 +294,13 @@ class ContactChatViewController: UITableViewController {
     
     private func getGroupRecursive(fmdb: FMDatabase, id: String = "", parent: String = "") -> [Group] {
         var data: [Group] = []
-        var query = "select g.group_id, g.f_name, g.image_id, g.quote, g.created_by, g.created_date, g.parent, g.group_type, g.is_open, g.official, g.is_education from GROUPZ g where "
+        var query = "select g.group_id, g.f_name, g.image_id, g.quote, g.created_by, g.created_date, g.parent, g.group_type, g.is_open, g.official, g.is_education, g.level from GROUPZ g where "
         if id.isEmpty {
             query += "g.parent = '\(parent)'"
         } else {
             query += "g.group_id = '\(id)'"
         }
+        query += "order by 10 desc"
         if let cursor = Database.shared.getRecords(fmdb: fmdb, query: query) {
             while cursor.next() {
                 let group = Group(
@@ -314,10 +315,11 @@ class ContactChatViewController: UITableViewController {
                     groupType: cursor.string(forColumnIndex: 7) ?? "",
                     isOpen: cursor.string(forColumnIndex: 8) ?? "",
                     official: cursor.string(forColumnIndex: 9) ?? "",
-                    isEducation: cursor.string(forColumnIndex: 10) ?? "")
+                    isEducation: cursor.string(forColumnIndex: 10) ?? "",
+                    level: cursor.string(forColumnIndex: 11) ?? "")
                 
                 if group.chatId.isEmpty {
-                    let lounge = Group(id: group.id, name: "Lounge".localized(), profile: "", quote: group.quote, by: group.by, date: group.date, parent: group.id, chatId: group.chatId, groupType: group.groupType, isOpen: group.isOpen, official: group.official, isEducation: group.isEducation, isLounge: true)
+                    let lounge = Group(id: group.id, name: "Lounge".localized(), profile: "", quote: group.quote, by: group.by, date: group.date, parent: group.id, chatId: group.chatId, groupType: group.groupType, isOpen: group.isOpen, official: group.official, isEducation: group.isEducation, isLounge: true, level: group.level != "-1" ? group.level : "2")
                     group.childs.append(lounge)
                 }
                 
@@ -334,29 +336,31 @@ class ContactChatViewController: UITableViewController {
                                           groupType: group.groupType,
                                           isOpen: group.isOpen,
                                           official: group.official,
-                                          isEducation: group.isEducation)
+                                          isEducation: group.isEducation,
+                                          level: group.level != "-1" ? group.level : "2")
                         group.childs.append(topic)
                     }
                     topicCursor.close()
                 }
                 
                 if !group.id.isEmpty {
-                    if group.official == "1" {
-                        let idMe = UserDefaults.standard.string(forKey: "me") as String?
-                        if let cursorUser = Database.shared.getRecords(fmdb: fmdb, query: "SELECT user_type FROM BUDDY where f_pin='\(idMe!)'"), cursorUser.next() {
-//                            if cursorUser.string(forColumnIndex: 0) == "23" || cursorUser.string(forColumnIndex: 0) == "24" {
-//                                group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
-//                            }
-                            group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
-                            cursorUser.close()
-                        }
-                    } else if group.official != "1"{
-                        group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
-                    }
-                    group.childs = group.childs.sorted(by: { $0.name < $1.name })
-                    let dataLounge = group.childs.filter({$0.name == "Lounge".localized()})
-                    group.childs = group.childs.filter({ $0.name != "Lounge".localized() })
-                    group.childs.insert(contentsOf: dataLounge, at: 0)
+//                    if group.official == "1" {
+//                        let idMe = UserDefaults.standard.string(forKey: "me") as String?
+//                        if let cursorUser = Database.shared.getRecords(fmdb: fmdb, query: "SELECT user_type FROM BUDDY where f_pin='\(idMe!)'"), cursorUser.next() {
+////                            if cursorUser.string(forColumnIndex: 0) == "23" || cursorUser.string(forColumnIndex: 0) == "24" {
+////                                group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
+////                            }
+//                            group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
+//                            cursorUser.close()
+//                        }
+//                    } else if group.official != "1"{
+//                        group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
+//                    }
+                    group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
+//                    group.childs = group.childs.sorted(by: { $0.name < $1.name })
+//                    let dataLounge = group.childs.filter({$0.name == "Lounge".localized()})
+//                    group.childs = group.childs.filter({ $0.name != "Lounge".localized() })
+//                    group.childs.insert(contentsOf: dataLounge, at: 0)
                 }
                 data.append(group)
             }
@@ -498,130 +502,144 @@ extension ContactChatViewController {
             editorPersonalVC.unique_l_pin = data.pin
             navigationController?.show(editorPersonalVC, sender: nil)
         case 2:
-            let group: Group
-            if isFilltering {
-                if indexPath.row == 0 {
-                    group = fillteredData[indexPath.section] as! Group
-                } else {
-                    group = (fillteredData[indexPath.section] as! Group).childs[indexPath.row - 1]
-                }
+            expandCollapseGroup(tableView: tableView, indexPath: indexPath)
+        default:
+            let data = contacts[indexPath.row]
+            let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
+            editorPersonalVC.hidesBottomBarWhenPushed = true
+            editorPersonalVC.unique_l_pin = data.pin
+            navigationController?.show(editorPersonalVC, sender: nil)
+        }
+    }
+    
+    func expandCollapseGroup(tableView: UITableView, indexPath: IndexPath) {
+        let group: Group
+        if isFilltering {
+            if indexPath.row == 0 {
+                group = fillteredData[indexPath.section] as! Group
             } else {
-                if indexPath.row == 0 {
-                    group = groups[indexPath.section]
-                } else {
-                    group = groups[indexPath.section].childs[indexPath.row - 1]
-                }
+                group = (fillteredData[indexPath.section] as! Group).childs[indexPath.row - 1]
             }
-            group.isSelected = !group.isSelected
-            if !group.isSelected{
-                var sects = 0
-                var sect = indexPath.section
-                var id = group.id
-                if let _ = groupMap[id] {
-                    var loooop = true
-                    repeat {
-                        let c = sect + 1
-                        if isFilltering {
-                            if let o = self.fillteredData[c] as? Group {
-                                if o.parent == id {
-                                    sects = sects + 1
-                                    sect = c
-                                    id = o.id
-                                    (self.fillteredData[c] as! Group).isSelected = false
-                                    self.groupMap.removeValue(forKey: (self.fillteredData[c] as! Group).id)
-                                }
-                                else {
-                                    loooop = false
-                                }
-                            }
-                        }
-                        else {
-                            if self.groups[c].parent == id {
+        } else {
+            if indexPath.row == 0 {
+                group = groups[indexPath.section]
+            } else {
+                group = groups[indexPath.section].childs[indexPath.row - 1]
+            }
+        }
+        group.isSelected = !group.isSelected
+        if !group.isSelected{
+            var sects = 0
+            var sect = indexPath.section
+            var id = group.id
+            if let _ = groupMap[id] {
+                var loooop = true
+                repeat {
+                    let c = sect + 1
+                    if isFilltering {
+                        if let o = self.fillteredData[c] as? Group {
+                            if o.parent == id {
                                 sects = sects + 1
                                 sect = c
-                                id = self.groups[c].id
-                                self.groups[c].isSelected = false
-                                self.groupMap.removeValue(forKey: self.groups[c].id)
+                                id = o.id
+                                (self.fillteredData[c] as! Group).isSelected = false
+                                self.groupMap.removeValue(forKey: (self.fillteredData[c] as! Group).id)
                             }
                             else {
                                 loooop = false
                             }
                         }
-                    } while(loooop)
-                }
-                for i in stride(from: sects, to: 0, by: -1){
-                    if isFilltering {
-                        self.fillteredData.remove(at: indexPath.section + i)
                     }
                     else {
-                        self.groups.remove(at: indexPath.section + i)
+                        if c < self.groups.count && self.groups[c].parent == id {
+                            sects = sects + 1
+                            sect = c
+                            id = self.groups[c].id
+                            self.groups[c].isSelected = false
+                            self.groupMap.removeValue(forKey: self.groups[c].id)
+                        }
+                        else {
+                            loooop = false
+                        }
                     }
+                } while(loooop)
+            }
+            for i in stride(from: sects, to: 0, by: -1){
+                if isFilltering {
+                    self.fillteredData.remove(at: indexPath.section + i)
+                }
+                else {
+                    self.groups.remove(at: indexPath.section + i)
                 }
-                groupMap.removeValue(forKey: group.id)
-            }
-            if group.groupType == "NOTJOINED" {
-                let alert = UIAlertController(title: "Do you want to join this group?".localized(), message: "Groups : \(group.name)\nMembers: \(group.by)".localized(), preferredStyle: .alert)
-                alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
-                alert.addAction(UIAlertAction(title: "Join".localized(), style: .default, handler: {(_) in
-                    self.joinOpenGroup(groupId: group.id, completion: { result in
-                        if result {
-                            DispatchQueue.main.async {
-                                self.groupMap.removeAll()
-                                let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
-                                editorGroupVC.hidesBottomBarWhenPushed = true
-                                editorGroupVC.unique_l_pin = group.id
-                                self.navigationController?.show(editorGroupVC, sender: nil)
-                            }
-                        }
-                    })
-                }))
-                self.present(alert, animated: true, completion: nil)
-                return
             }
-            if group.childs.count == 0 {
-                let groupId = group.chatId.isEmpty ? group.id : group.chatId
-                if let chooser = isChooser {
-                    chooser("4", groupId)
-                    dismiss(animated: true, completion: nil)
-                    return
+            groupMap.removeValue(forKey: group.id)
+        }
+        if group.groupType == "NOTJOINED" {
+            let alert = UIAlertController(title: "Do you want to join this group?".localized(), message: "Groups : \(group.name)\nMembers: \(group.by)".localized(), preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
+            alert.addAction(UIAlertAction(title: "Join".localized(), style: .default, handler: {(_) in
+                self.joinOpenGroup(groupId: group.id, completion: { result in
+                    if result {
+                        DispatchQueue.main.async {
+                            self.groupMap.removeAll()
+                            let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
+                            editorGroupVC.hidesBottomBarWhenPushed = true
+                            editorGroupVC.unique_l_pin = group.id
+                            self.navigationController?.show(editorGroupVC, sender: nil)
+                        }
+                    }
+                })
+            }))
+            self.present(alert, animated: true, completion: nil)
+            return
+        }
+        if group.childs.count == 0 {
+            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                let idMe = UserDefaults.standard.string(forKey: "me") as String?
+                if let cursorMember = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin from GROUPZ_MEMBER where group_id = '\(group.id)' and f_pin = '\(idMe!)'"), cursorMember.next() {
+                    let groupId = group.chatId.isEmpty ? group.id : group.chatId
+                    if let chooser = isChooser {
+                        chooser("4", groupId)
+                        dismiss(animated: true, completion: nil)
+                        return
+                    }
+                    self.groupMap.removeAll()
+                    let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
+                    editorGroupVC.hidesBottomBarWhenPushed = true
+                    editorGroupVC.unique_l_pin = groupId
+                    navigationController?.show(editorGroupVC, sender: nil)
+                    cursorMember.close()
+                } else {
+                    self.view.makeToast("You are not a member of this group".localized(), duration: 0.5)
                 }
-                self.groupMap.removeAll()
-                let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
-                editorGroupVC.hidesBottomBarWhenPushed = true
-                editorGroupVC.unique_l_pin = groupId
-                navigationController?.show(editorGroupVC, sender: nil)
+            })
+        } else {
+            if indexPath.row == 0 {
+                tableView.reloadData()
             } else {
-                if indexPath.row == 0 {
-                    tableView.reloadData()
-                } else {
-                    getGroups(id: group.id) { g in
-                        DispatchQueue.main.async {
-                            print("index path section: \(indexPath.section)")
-                            print("index path row: \(indexPath.row)")
+                getGroups(id: group.id) { g in
+                    DispatchQueue.main.async {
+                        print("index path section: \(indexPath.section)")
+                        print("index path row: \(indexPath.row)")
 //                            print("index path item: \(indexPath.item)")
-                            if self.isFilltering {
+                        if self.isFilltering {
 //                                self.fillteredData.remove(at: indexPath.section)
-                                if self.fillteredData[indexPath.section] is Group {
-                                    self.groupMap[(self.fillteredData[indexPath.section] as! Group).id] = 1
-                                    self.fillteredData.insert(contentsOf: g, at: indexPath.section + 1)
-                                }
-                            } else {
-//                                self.groups.remove(at: indexPath.section)
-                                self.groupMap[self.groups[indexPath.section].id] = 1
-                                self.groups.insert(contentsOf: g, at: indexPath.section + 1)
+                            if self.fillteredData[indexPath.section] is Group {
+                                self.groupMap[(self.fillteredData[indexPath.section] as! Group).id] = 1
+                                self.fillteredData.insert(contentsOf: g, at: indexPath.section + 1)
                             }
-                            print("groupMap: \(self.groupMap)")
-                            tableView.reloadData()
+                        } else {
+//                                self.groups.remove(at: indexPath.section)
+                            self.groupMap[self.groups[indexPath.section].id] = 1
+                            self.groups.insert(contentsOf: g, at: indexPath.section + 1)
                         }
+                        print("groupMap: \(self.groupMap)")
+                        tableView.reloadData()
+                        
+                        self.expandCollapseGroup(tableView: tableView, indexPath: IndexPath(row: 0, section: indexPath.section + 1))
                     }
                 }
             }
-        default:
-            let data = contacts[indexPath.row]
-            let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
-            editorPersonalVC.hidesBottomBarWhenPushed = true
-            editorPersonalVC.unique_l_pin = data.pin
-            navigationController?.show(editorPersonalVC, sender: nil)
         }
     }
     
@@ -920,6 +938,11 @@ extension ContactChatViewController {
                 content.image = image
             }
             cell.contentConfiguration = content
+            if group.level != "-1" && Int(group.level)! < 7 {
+                cell.contentView.layoutMargins = .init(top: 0.0, left: CGFloat(25 * Int(group.level)!), bottom: 0.0, right: 0)
+            } else if Int(group.level)! > 6 {
+                cell.contentView.layoutMargins = .init(top: 0.0, left: CGFloat(25 * (Int(group.level)! - 6)), bottom: 0.0, right: 0)
+            }
         default:
             cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifierContact", for: indexPath)
             var content = cell.defaultContentConfiguration()

+ 1 - 1
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/GroupDetailViewController.swift

@@ -20,7 +20,7 @@ class GroupDetailViewController: UITableViewController {
     
     var data: String = ""
     
-    static let SUBGROUP_LEVEL_LIMIT = 5
+    static let SUBGROUP_LEVEL_LIMIT = 10
     
     private var group: Group?
     

+ 110 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/NotificationSound.swift

@@ -0,0 +1,110 @@
+//
+//  NotificationSound.swift
+//  NexilisLite
+//
+//  Created by Akhmad Al Qindi Irsyam on 28/11/22.
+//
+
+import UIKit
+import AVFoundation
+
+public class NotificationSound: UIViewController, UITableViewDelegate, UITableViewDataSource {
+    
+    public var isPersonal = true
+    let tableView = UITableView()
+    var data: [NotifSound] = []
+    var isSelectedSound = 0
+
+    public override func viewDidLoad() {
+        super.viewDidLoad()
+
+        self.view.backgroundColor = .white
+        
+        view.addSubview(tableView)
+        tableView.translatesAutoresizingMaskIntoConstraints = false
+        tableView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor).isActive = true
+        tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
+        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
+        tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
+        
+        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellNotifSound")
+        
+        tableView.delegate = self
+        tableView.dataSource = self
+
+        self.title = "Notification Message(s)".localized()
+        if !isPersonal {
+            self.title = "Notification Message(s) Group".localized()
+        }
+        navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(cancel(sender:)))
+        
+        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save".localized(), style: .plain, target: self, action: #selector(save(sender:)))
+        data.append(NotifSound(id: 1007, name: "sms-received1 (Default)", isSelected: false))
+        data.append(NotifSound(id: 1008, name: "sms-received2", isSelected: false))
+        data.append(NotifSound(id: 1009, name: "sms-received3", isSelected: false))
+        data.append(NotifSound(id: 1010, name: "sms-received4", isSelected: false))
+        data.append(NotifSound(id: 1013, name: "sms-received5", isSelected: false))
+        data.append(NotifSound(id: 1014, name: "sms-received6", isSelected: false))
+        var selectedSound = UserDefaults.standard.string(forKey: "notifSoundPersonal") ?? ""
+        if !isPersonal {
+            selectedSound = UserDefaults.standard.string(forKey: "notifSoundGroup") ?? ""
+        }
+        if !selectedSound.isEmpty {
+            let selectedSoundId = Int(selectedSound.components(separatedBy: ":")[0])
+            let idx = data.firstIndex(where: {$0.id == selectedSoundId})
+            if idx != nil {
+                data[idx!].isSelected = true
+                isSelectedSound = selectedSoundId!
+            } else {
+                data[0].isSelected = true
+                isSelectedSound = 1007
+            }
+        } else {
+            data[0].isSelected = true
+            isSelectedSound = 1007
+        }
+    }
+    
+    @objc func cancel(sender: Any) {
+        navigationController?.dismiss(animated: true, completion: nil)
+    }
+    
+    @objc func save(sender: Any) {
+        let idx = data.firstIndex(where: {$0.id == isSelectedSound})
+        if isPersonal {
+            UserDefaults.standard.set("\(data[idx!].id):\(data[idx!].name)", forKey: "notifSoundPersonal")
+        } else {
+            UserDefaults.standard.set("\(data[idx!].id):\(data[idx!].name)", forKey: "notifSoundGroup")
+        }
+        navigationController?.dismiss(animated: true, completion: nil)
+    }
+    
+    // MARK: - Table view data source
+    
+    public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        let idx = data.firstIndex(where: {$0.id == isSelectedSound})
+        data[idx!].isSelected = false
+        
+        let idxNew = data.firstIndex(where: {$0.id == data[indexPath.row].id})
+        data[idxNew!].isSelected = true
+        isSelectedSound = data[indexPath.row].id
+        
+        let systemSoundID: SystemSoundID = SystemSoundID(isSelectedSound)
+        AudioServicesPlaySystemSound(systemSoundID)
+        tableView.reloadData()
+    }
+
+    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return data.count
+    }
+
+    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let cell = tableView.dequeueReusableCell(withIdentifier: "cellNotifSound", for: indexPath)
+        var content = cell.defaultContentConfiguration()
+        content.text = data[indexPath.row].name
+        cell.contentConfiguration = content
+        cell.accessoryType = data[indexPath.row].isSelected ? .checkmark : .none
+        cell.selectionStyle = .none
+        return cell
+    }
+}