ソースを参照

add new features and for release 5.0.47

alqindiirsyam 1 ヶ月 前
コミット
3b9fc87fc3

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

@@ -564,7 +564,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.46;
+				MARKETING_VERSION = 5.0.47;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -600,7 +600,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.46;
+				MARKETING_VERSION = 5.0.47;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -636,7 +636,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.46;
+				MARKETING_VERSION = 5.0.47;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -675,7 +675,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.46;
+				MARKETING_VERSION = 5.0.47;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 2 - 2
AppBuilder/AppBuilder/SecondTabViewController.swift

@@ -908,7 +908,7 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
             let allChats = Chat.getData()
             self.archivedChats = Chat.getData(isArchived: true)
             var tempChats: [Chat] = []
-            var lowestPinned: [String: Int] = [:]
+            var lowestPinned: [String: Int64] = [:]
 
             for singleChat in allChats {
                 guard !singleChat.groupId.isEmpty else {
@@ -2175,7 +2175,7 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                             stringMessage.append(("🚫 _"+"You were deleted this message".localized()+"_").richText())
                         } else {
                             let imageStatus = NSTextAttachment()
-                            if data.messageScope != MessageScope.CALL && data.messageScope != MessageScope.MISSED_CALL {
+                            if data.messageScope != MessageScope.CALL && data.messageScope != MessageScope.MISSED_CALL && !data.messageId.contains("NTFPIN_") {
                                 let status = getRealStatus(messageId: data.messageId)
                                 if status == "0" {
                                     imageStatus.image = UIImage(systemName: "xmark.circle")!.withTintColor(UIColor.red, renderingMode: .alwaysOriginal)

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

@@ -2769,4 +2769,19 @@ public class CoreMessage_TMessageBank {
         return tMessage
     }
     
+    public static func getPinMessage(f_pin: String, data: String, oppositePin: String, chatId: String, scopeId: String) -> TMessage {
+        let tMessage = NexilisLite.TMessage()
+        let me = User.getMyPin() ?? ""
+        tMessage.mPIN = me
+        tMessage.mCode = CoreMessage_TMessageCode.UPDATE_MESSAGE
+        tMessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tMessage.mBodies[CoreMessage_TMessageKey.F_PIN] = f_pin
+        tMessage.mBodies[CoreMessage_TMessageKey.DATA] = data
+        tMessage.mBodies[CoreMessage_TMessageKey.OPPOSITE_PIN] = oppositePin
+        tMessage.mBodies[CoreMessage_TMessageKey.CHAT_ID] = chatId
+        tMessage.mBodies[CoreMessage_TMessageKey.SCOPE_ID] = scopeId
+        tMessage.mBodies[CoreMessage_TMessageKey.ITEM_CODE] = "pinorunpin"
+        return tMessage
+    }
+    
 }

+ 1 - 0
NexilisLite/NexilisLite/Source/CoreMessage_TMessageKey.swift

@@ -354,6 +354,7 @@ public class CoreMessage_TMessageKey {
     public static let SEARCH_MESSAGE_FLAG = "C05"
     public static let VIEW_MEDIA_FLAG = "C06"
     public static let IS_STARED_MESSAGE = "IS1"
+    public static let IS_PINNED_MESSAGE = "IS2"
     public static let FROM_WEB = "W01"
     public static let STATUS_FORM = "SF"
     public static let POP_HOST = "EM1"

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

@@ -163,12 +163,12 @@ extension UIView {
         }
         if width != 0 || minWidth != 0 || maxWidth != 0 {
             if minWidth != 0 && maxWidth != 0 {
-                heightAnchor.constraint(greaterThanOrEqualToConstant: minWidth).isActive = true
-                heightAnchor.constraint(lessThanOrEqualToConstant: maxWidth).isActive = true
+                widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth).isActive = true
+                widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth).isActive = true
             } else if minWidth != 0 && maxWidth == 0 {
-                heightAnchor.constraint(greaterThanOrEqualToConstant: minWidth).isActive = true
+                widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth).isActive = true
             } else if minWidth == 0 && maxWidth != 0 {
-                heightAnchor.constraint(lessThanOrEqualToConstant: maxWidth).isActive = true
+                widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth).isActive = true
             } else {
                 widthAnchor.constraint(equalToConstant: width).isActive = true
             }

+ 43 - 0
NexilisLite/NexilisLite/Source/IncomingThread.swift

@@ -195,6 +195,8 @@ class IncomingThread {
             showTransactionApprovalRequest(message: message)
         } else if message.getCode() == CoreMessage_TMessageCode.LOGOUT {
             logoutDevice(message: message)
+        } else if message.getCode() == CoreMessage_TMessageCode.UPDATE_MESSAGE {
+            updateMessage(message: message)
         } else {
             //print("unprocessed code", message.getCode())
             ack(message: message)
@@ -208,6 +210,47 @@ class IncomingThread {
      *
      */
     
+    private func updateMessage(message: TMessage) -> Void {
+        let data = message.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "[]")
+        let item_code = message.getBody(key: CoreMessage_TMessageKey.ITEM_CODE, default_value: "")
+        let f_pin = message.getBody(key: CoreMessage_TMessageKey.F_PIN, default_value: "")
+        let l_pin = message.getBody(key: CoreMessage_TMessageKey.OPPOSITE_PIN, default_value: "")
+        let chat_id = message.getBody(key: CoreMessage_TMessageKey.CHAT_ID, default_value: "")
+        let scope_id = message.getBody(key: CoreMessage_TMessageKey.SCOPE_ID, default_value: "")
+        if item_code == "pinorunpin" {
+            if !data.isEmpty {
+                if let jsonArray = try! JSONSerialization.jsonObject(with: data.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [AnyObject] {
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        do {
+                            for json in jsonArray {
+                                let pinned = CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.IS_PINNED_MESSAGE)
+                                let messageId = CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.MESSAGE_ID)
+                                var messageIdNotif = ""
+                                if pinned != "0" {
+                                    if let dataUser = User.getData(pin: f_pin, lPin: l_pin, fmdb: fmdb) {
+                                        messageIdNotif = Nexilis.saveMessageNotif(textMessage: dataUser.fullName + " " + "pinned a message".localized(), fPin: f_pin, lPin: l_pin, chatId: chat_id, scopeId: scope_id, fmdb: fmdb)
+                                    }
+                                }
+                                _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
+                                    "is_pinned" : pinned
+                                ], _where: "message_id = '\(messageId)'")
+                                var dataMessage: [AnyHashable : Any] = [:]
+                                dataMessage["message_id"] = messageId
+                                dataMessage["message_id_notif"] = messageIdNotif
+                                dataMessage["is_pinned"] = pinned
+                                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onUpdatedMessage"), object: nil, userInfo: dataMessage)
+                            }
+                            ack(message: message)
+                        } catch {
+                            rollback.pointee = true
+                            print("Access database error: \(error.localizedDescription)")
+                        }
+                    })
+                }
+            }
+        }
+    }
+    
     private func sendUpdateLiveStream(message: TMessage) -> Void {
         var dataMessage: [AnyHashable : Any] = [:]
         dataMessage["message"] = message

+ 56 - 4
NexilisLite/NexilisLite/Source/Model/Chat.swift

@@ -33,7 +33,7 @@ public class Chat: Model {
     public let groupName: String
     public var isSelected: Bool
     public var isParent: Bool
-    public var pinned: Int
+    public var pinned: Int64
     public var isFolPinned: Bool
     
     public init(pin: String) {
@@ -94,7 +94,7 @@ public class Chat: Model {
         self.isFolPinned = false
     }
     
-    public init(fpin:String, pin: String, messageId: String, counter: String, messageText: String, serverDate: String, image: String, video: String, file: String, attachmentFlag: String, messageScope: String, name: String, profile: String, official: String, status: String, credential: String, lock: String, thumb: String = "", audio: String = "", gif: String = "", groupId: String = "", groupName: String = "", isSelected: Bool = false, isParent: Bool = false, pinned: Int = 0, isFolPinned: Bool = false) {
+    public init(fpin:String, pin: String, messageId: String, counter: String, messageText: String, serverDate: String, image: String, video: String, file: String, attachmentFlag: String, messageScope: String, name: String, profile: String, official: String, status: String, credential: String, lock: String, thumb: String = "", audio: String = "", gif: String = "", groupId: String = "", groupName: String = "", isSelected: Bool = false, isParent: Bool = false, pinned: Int64 = 0, isFolPinned: Bool = false) {
         self.fpin = fpin
         self.pin = pin
         self.messageId = messageId
@@ -157,7 +157,7 @@ public class Chat: Model {
         var messages: [Chat] = []
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
-                let query = "m.f_pin, m.l_pin, m.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, b.first_name || ' ' || ifnull(b.last_name, '') name, b.image_id profile, b.official_account, m.status, m.credential, m.lock, m.audio_id, m.gif_id, '' group_id, '' group_name from MESSAGE m, BUDDY b where m.l_pin = b.f_pin and m.is_call_center = 0 and message_text LIKE '%\(text)%'"
+                let query = "select m.f_pin, m.l_pin, m.message_id, ms.counter, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, b.first_name || ' ' || ifnull(b.last_name, '') name, b.image_id profile, b.official_account, m.status, m.credential, m.lock, m.audio_id, m.gif_id, '' group_id, '' group_name from MESSAGE m, BUDDY b where m.l_pin = b.f_pin and m.is_call_center = 0 and message_text LIKE '%\(text)%'"
                 if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
                     while cursorData.next() {
                         let data = cursorData.string(forColumnIndex: 0) ?? ""
@@ -195,6 +195,58 @@ public class Chat: Model {
         return messages
     }
     
+    public static func getMessageFromId(message_id: String = "") -> [Chat] {
+        var messages: [Chat] = []
+        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+            do {
+                let query = """
+                            select m.f_pin, m.l_pin, m.message_id, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, b.first_name || ' ' || ifnull(b.last_name, '') name, b.image_id profile, b.official_account, m.status, m.credential, m.lock, m.audio_id, m.gif_id, '' group_id, '' group_name, m.thumb_id from MESSAGE m, BUDDY b where (m.l_pin = b.f_pin OR m.f_pin = b.f_pin) and m.attachment_flag = '3' and m.message_id = '\(message_id)' and m.is_call_center = 0
+                            union
+                            select m.f_pin, m.l_pin, m.message_id, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, 'Bot' name, '' profile, '', m.status, m.credential, m.lock, m.audio_id, m.gif_id, '' group_id, '' group_name, m.thumb_id from MESSAGE m where m.f_pin = '-999' and m.message_id = '\(message_id)' and m.is_call_center = 0
+                            union
+                            select m.f_pin, m.l_pin, m.message_id, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, 'GPT SmartBot' name, '' profile, '', m.status, m.credential, m.lock, m.audio_id, m.gif_id, '' group_id, '' group_name, m.thumb_id from MESSAGE m where m.f_pin = '-997' and m.message_id = '\(message_id)' and m.is_call_center = 0
+                            union
+                            select m.f_pin, m.l_pin, m.message_id, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, '\("Lounge".localized())' name, b.image_id profile, b.official, m.status, m.credential, m.lock, m.audio_id, m.gif_id, b.group_id, b.f_name group_name, m.thumb_id from MESSAGE m, GROUPZ b where m.l_pin = b.group_id and m.message_id = '\(message_id)' and m.is_call_center = 0
+                            union
+                            select m.f_pin, m.l_pin, m.message_id, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, b.title, c.image_id profile, '', m.status, m.credential, m.lock, m.audio_id, m.gif_id, c.group_id, c.f_name group_name, m.thumb_id from MESSAGE m, DISCUSSION_FORUM b, GROUPZ c where b.group_id = c.group_id and m.l_pin = b.chat_id and m.message_id = '\(message_id)' and m.is_call_center = 0
+                            order by 6 desc
+                            """
+                if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
+                    while cursorData.next() {
+                        let chat = Chat(fpin: cursorData.string(forColumnIndex: 0) ?? "",
+                                        pin: cursorData.string(forColumnIndex: 1) ?? "",
+                                        messageId: cursorData.string(forColumnIndex: 2) ?? "",
+                                        counter: "0",
+                                        messageText: cursorData.string(forColumnIndex: 3) ?? "",
+                                        serverDate: cursorData.string(forColumnIndex: 4) ?? "",
+                                        image: cursorData.string(forColumnIndex: 5) ?? "",
+                                        video: cursorData.string(forColumnIndex: 6) ?? "",
+                                        file: cursorData.string(forColumnIndex: 7) ?? "",
+                                        attachmentFlag: cursorData.string(forColumnIndex: 8) ?? "",
+                                        messageScope: cursorData.string(forColumnIndex: 9) ?? "",
+                                        name: cursorData.string(forColumnIndex: 10) ?? "",
+                                        profile: cursorData.string(forColumnIndex: 11) ?? "",
+                                        official: cursorData.string(forColumnIndex: 12) ?? "",
+                                        status: cursorData.string(forColumnIndex: 13) ?? "",
+                                        credential: cursorData.string(forColumnIndex: 14) ?? "",
+                                        lock: cursorData.string(forColumnIndex: 15) ?? "",
+                                        thumb: cursorData.string(forColumnIndex: 20) ?? "",
+                                        audio: cursorData.string(forColumnIndex: 16) ?? "",
+                                        gif: cursorData.string(forColumnIndex: 17) ?? "",
+                                        groupId: cursorData.string(forColumnIndex: 18) ?? "",
+                                        groupName: cursorData.string(forColumnIndex: 19) ?? "")
+                        messages.append(chat)
+                    }
+                    cursorData.close()
+                }
+            } catch {
+                rollback.pointee = true
+                print("Access database error: \(error.localizedDescription)")
+            }
+        })
+        return messages
+    }
+    
 //    public static func getUcList() {
 //        Database.shared.database?.inTransaction({ (fmdb, rollback) in
 //            let query = " select ms.message_id from MESSAGE_SUMMARY ms"
@@ -276,7 +328,7 @@ public class Chat: Model {
                                         gif: !lastQuery.isEmpty ? cursorData.string(forColumnIndex: 17) ?? "" : cursorData.string(forColumnIndex: 18) ?? "",
                                         groupId: cursorData.string(forColumnIndex: 19) ?? "",
                                         groupName: cursorData.string(forColumnIndex: 20) ?? "",
-                                        pinned: Int(cursorData.int(forColumnIndex: 21)))
+                                        pinned: cursorData.longLongInt(forColumnIndex: 21))
                         chats.append(chat)
                     }
                     cursorData.close()

+ 119 - 1
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -19,7 +19,7 @@ import CryptoKit
 import WebKit
 
 public class Nexilis: NSObject {
-    public static var cpaasVersion = "5.0.46"
+    public static var cpaasVersion = "5.0.47"
     public static var sAPIKey = ""
     
     public static var ADDRESS = ""
@@ -1901,6 +1901,124 @@ public class Nexilis: NSObject {
         //print("insert db message summary \(message_id)")
     }
     
+    public static func saveMessageNotif(textMessage: String, fPin: String, lPin: String, chatId: String, scopeId: String, fmdb: FMDatabase? = nil) -> String {
+        guard let me = User.getMyPin() else {
+            return ""
+        }
+        let dataFpin = User.getData(pin: fPin, lPin: lPin, fmdb: fmdb)
+        let dataLpin = User.getData(pin: lPin, fmdb: fmdb)
+        let message_id = "NTFPIN_" + CoreMessage_TMessageUtil.getTID()
+        if fmdb == nil {
+            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                do {
+                    _ = try Database.shared.insertRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
+                        "message_id" : message_id,
+                        "f_pin" : fPin,
+                        "f_display_name" : dataFpin != nil ? dataFpin!.fullName : "",
+                        "l_pin" : lPin,
+                        "l_user_id" : dataLpin != nil ? dataLpin!.pin : "",
+                        "message_scope_id" : scopeId,
+                        "server_date" : Date().currentTimeMillis(),
+                        "status" : "3",
+                        "message_text" : textMessage,
+                        "audio_id" : "",
+                        "video_id" : "",
+                        "image_id" : "",
+                        "file_id" : "",
+                        "thumb_id" : "",
+                        "opposite_pin" : "",
+                        "format" : "",
+                        "blog_id" : "",
+                        "read_receipts" : "0",
+                        "chat_id" : chatId,
+                        "account_type" : "1",
+                        "credential" :"",
+                        "reff_id" : "",
+                        "message_large_text" : "",
+                        "attachment_flag" : "",
+                        "local_timestamp" : ""
+                    ], replace: true)
+                } catch {
+                    rollback.pointee = true
+                    print("Access database error: \(error.localizedDescription)")
+                }
+            })
+        } else {
+            do {
+                _ = try Database.shared.insertRecord(fmdb: fmdb!, table: "MESSAGE", cvalues: [
+                    "message_id" : message_id,
+                    "f_pin" : fPin,
+                    "f_display_name" : dataFpin != nil ? dataFpin!.fullName : "",
+                    "l_pin" : lPin,
+                    "l_user_id" : dataLpin != nil ? dataLpin!.pin : "",
+                    "message_scope_id" : scopeId,
+                    "server_date" : Date().currentTimeMillis(),
+                    "status" : "3",
+                    "message_text" : textMessage,
+                    "audio_id" : "",
+                    "video_id" : "",
+                    "image_id" : "",
+                    "file_id" : "",
+                    "thumb_id" : "",
+                    "opposite_pin" : "",
+                    "format" : "",
+                    "blog_id" : "",
+                    "read_receipts" : "0",
+                    "chat_id" : chatId,
+                    "account_type" : "1",
+                    "credential" :"",
+                    "reff_id" : "",
+                    "message_large_text" : "",
+                    "attachment_flag" : "",
+                    "local_timestamp" : ""
+                ], replace: true)
+            } catch {
+                print("Access database error: \(error.localizedDescription)")
+            }
+        }
+        let pin = lPin == me ? fPin : lPin
+        var pinned = 0
+        var archived = 0
+        if fmdb == nil {
+            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                do {
+                    if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select pinned, archived from MESSAGE_SUMMARY where l_pin = '\(pin)'"), cursor.next() {
+                        pinned = Int(cursor.int(forColumnIndex: 0))
+                        archived = Int(cursor.int(forColumnIndex: 1))
+                    }
+                    _ = try Database.shared.insertRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
+                        "l_pin" : pin,
+                        "message_id" : message_id,
+                        "counter" : 0,
+                        "pinned" : pinned,
+                        "archived" : archived
+                    ], replace: true)
+                } catch {
+                    rollback.pointee = true
+                    print("Access database error: \(error.localizedDescription)")
+                }
+            })
+        } else {
+            do {
+                if let cursor = Database.shared.getRecords(fmdb: fmdb!, query: "select pinned, archived from MESSAGE_SUMMARY where l_pin = '\(pin)'"), cursor.next() {
+                    pinned = Int(cursor.int(forColumnIndex: 0))
+                    archived = Int(cursor.int(forColumnIndex: 1))
+                }
+                _ = try Database.shared.insertRecord(fmdb: fmdb!, table: "MESSAGE_SUMMARY", cvalues: [
+                    "l_pin" : pin,
+                    "message_id" : message_id,
+                    "counter" : 0,
+                    "pinned" : pinned,
+                    "archived" : archived
+                ], replace: true)
+            } catch {
+                print("Access database error: \(error.localizedDescription)")
+            }
+        }
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
+        return message_id
+    }
+    
     public static func saveMessageCall(idCall: String, textMessage: String, fPin: String, lPin: String, timeCall: String, attachment_type:String) {
         guard let me = User.getMyPin() else {
             return

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

@@ -3214,3 +3214,44 @@ class ZoomTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate
             return animator
     }
 }
+
+public class CallBannerView: UIView {
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        backgroundColor = UIColor.systemGreen
+
+        let label = UILabel()
+        label.text = "Ardi easySoft - Ringing"
+        label.textColor = .white
+        label.font = UIFont.boldSystemFont(ofSize: 16)
+
+        let endCallButton = UIButton(type: .system)
+        endCallButton.setImage(UIImage(systemName: "phone.down.fill"), for: .normal)
+        endCallButton.tintColor = .white
+        endCallButton.addTarget(self, action: #selector(endCallTapped), for: .touchUpInside)
+
+        let stack = UIStackView(arrangedSubviews: [label, endCallButton])
+        stack.axis = .horizontal
+        stack.alignment = .center
+        stack.distribution = .equalSpacing
+        stack.spacing = 12
+
+        addSubview(stack)
+        stack.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
+            stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
+            stack.topAnchor.constraint(equalTo: topAnchor, constant: 10),
+            stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10)
+        ])
+    }
+
+    @objc func endCallTapped() {
+        print("Call ended")
+        self.removeFromSuperview()
+    }
+
+    public required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}

+ 16 - 0
NexilisLite/NexilisLite/Source/View/Call/CallManager.swift

@@ -37,6 +37,22 @@ public class CallManager: NSObject, ObservableObject {
         self.provider.setDelegate(self, queue: nil)
     }
     
+    func startCall(uuid: UUID, callerName: String, callerId: String, isVideo: Bool) {
+        let handle = CXHandle(type: .generic, value: callerId)
+        let startCallAction = CXStartCallAction(call: uuid, handle: handle)
+        startCallAction.isVideo = isVideo
+        
+        let transaction = CXTransaction(action: startCallAction)
+        
+        callController.request(transaction) { error in
+            if let error = error {
+                print("Error starting call: \(error.localizedDescription)")
+            } else {
+                print("Call started successfully")
+            }
+        }
+    }
+    
     public func reportIncomingCall(uuid: UUID, callerName: String, callerId: String, isVideo: Bool) {
         let update = CXCallUpdate()
         update.remoteHandle = CXHandle(type: .generic, value: callerId)

+ 44 - 8
NexilisLite/NexilisLite/Source/View/Call/QmeraAudioViewController.swift

@@ -252,6 +252,17 @@ class QmeraAudioViewController: UIViewController {
         return button
     }()
     
+    let minimizeLogo: UIButton = {
+        let button = UIButton()
+        button.setImage(UIImage(systemName: "arrow.down.right.and.arrow.up.left")?.withTintColor(.white, renderingMode: .alwaysOriginal), for: .normal)
+        button.imageView?.contentMode = .scaleAspectFit
+        button.setBackgroundColor(.gray.withAlphaComponent(0.4), for: .normal)
+        button.contentVerticalAlignment = .fill
+        button.contentHorizontalAlignment = .fill
+        button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
+        return button
+    }()
+    
     static func turnSpeakerOn() {
 //        var bAudioEngineIsAvtive: Bool! = false
 //        API.turnSpeakerPhone(bSPon: bSpeakerPhone)
@@ -309,6 +320,17 @@ class QmeraAudioViewController: UIViewController {
         AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: "outputVolume")
     }
     
+    func showCallBanner() {
+        guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { return }
+
+        let bannerHeight: CGFloat = 60
+        let banner = CallBannerView(frame: CGRect(x: 0, y: 0, width: window.frame.width, height: bannerHeight))
+
+        banner.frame.origin.y = window.safeAreaInsets.top
+
+        window.addSubview(banner)
+    }
+    
     private func backToDefaultAudioSession() {
         do {
             let audioSession = AVAudioSession.sharedInstance()
@@ -341,6 +363,11 @@ class QmeraAudioViewController: UIViewController {
         view.addSubview(profiles)
         view.addSubview(name)
         
+        view.addSubview(minimizeLogo)
+        minimizeLogo.anchor(top: view.topAnchor, left: view.leftAnchor, paddingTop: 30, paddingLeft: 20, width: 40, height: 40)
+        minimizeLogo.addTarget(self, action: #selector(didMinimized(sender:)), for: .touchUpInside)
+        minimizeLogo.isHidden = true
+        
         status.anchor(left: view.leftAnchor, bottom: profiles.topAnchor, right: view.rightAnchor, paddingBottom: 30, centerX: view.centerXAnchor)
         profiles.anchor(centerX: view.centerXAnchor, centerY: view.centerYAnchor, width: 150, height: 150)
         name.anchor(top: profiles.bottomAnchor, left: view.leftAnchor, right: view.rightAnchor, paddingTop: 5, paddingLeft: 20, paddingRight: 20, centerX: view.centerXAnchor)
@@ -396,10 +423,12 @@ class QmeraAudioViewController: UIViewController {
                                 }
                             }
                         } else {
-                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
-                            imageView.tintColor = .white
-                            let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
-                            banner.show()
+                            DispatchQueue.main.async {
+                                let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                                imageView.tintColor = .white
+                                let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                                banner.show()
+                            }
                         }
                     }
                 } else {
@@ -493,6 +522,7 @@ class QmeraAudioViewController: UIViewController {
         invite.circle()
         speaker.circle()
         mic.circle()
+        minimizeLogo.circle()
     }
     
     private func getUserData(completion: @escaping (User?) -> ()) {
@@ -820,10 +850,12 @@ class QmeraAudioViewController: UIViewController {
                                 Nexilis.stopRingbacktoneCall()
                             }
                         } else {
-                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
-                            imageView.tintColor = .white
-                            let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
-                            banner.show()
+                            DispatchQueue.main.async {
+                                let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                                imageView.tintColor = .white
+                                let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                                banner.show()
+                            }
                         }
                     }
                 } else {
@@ -837,6 +869,10 @@ class QmeraAudioViewController: UIViewController {
         present(CustomNavigationController(rootViewController: controller), animated: true, completion: nil)
     }
     
+    @objc func didMinimized(sender: Any?) {
+        
+    }
+    
     @objc func didPressEnd(sender: Any?) {
         if let sharedAudioPlayer = Nexilis.sharedAudioPlayer, sharedAudioPlayer.isPlaying {
             Nexilis.stopRingtoneCall()

+ 493 - 11
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -72,6 +72,9 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     let viewSticker = UIView()
     let containerLink = UIView()
     let containerPreviewReply = UIView()
+    let containerPin = UIView()
+    let textPin = UILabel()
+    let signSelectedPin = UIStackView()
     var bottomAnchorPreviewReply = NSLayoutConstraint()
     let containerAction = UIView()
     var allowTyping = true
@@ -82,6 +85,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     var textSearch = ""
     var countMatchesSearch = 0
     var lastScrollIdxSearch = 0
+    var nextPinShowed = 0
     var buttonUp: UIButton!
     var buttonDown: UIButton!
     var keyboardHeightForMention: CGFloat?
@@ -291,6 +295,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         center.addObserver(self, selector: #selector(onGroup(notification:)), name: NSNotification.Name(rawValue: "onGroup"), object: nil)
         center.addObserver(self, selector: #selector(onMemberTopic(notification:)), name: NSNotification.Name(rawValue: "onTopic"), object: nil)
         center.addObserver(self, selector: #selector(onFailedSendMessage(notification:)), name: NSNotification.Name(rawValue: Nexilis.failedSendMessage), object: nil)
+        center.addObserver(self, selector: #selector(onUpdatedMessage(notification:)), name: NSNotification.Name(rawValue: "onUpdatedMessage"), object: nil)
         
         locationManager.delegate = self
         locationManager.requestWhenInUseAuthorization()
@@ -613,6 +618,8 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                 })
             }
         }
+        let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+        pinAllMessages(dataMessages: dataMessagesPin)
     }
     
     func getDataProfile(f_pin: String, message_id: String) -> [String: String]{
@@ -673,11 +680,11 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     private func getData() {
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
-                var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality FROM MESSAGE where chat_id='' AND l_pin='\(dataGroup["group_id"]  as? String ?? "")' order by server_date asc"
+                var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='' AND l_pin='\(dataGroup["group_id"]  as? String ?? "")' order by server_date asc"
                 if isHistoryCC {
                     query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared FROM MESSAGE where call_center_id='\(complaintId)' order by server_date asc"
                 } else if (dataTopic["chat_id"]  as? String ?? "" != "") {
-                    query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message FROM MESSAGE where chat_id='\(dataTopic["chat_id"]  as? String ?? "")' order by server_date asc"
+                    query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='\(dataTopic["chat_id"]  as? String ?? "")' order by server_date asc"
                 }
                 if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
                     var tempImages: [ImageGrouping] = []
@@ -701,12 +708,13 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                         row["reff_id"] = cursorData.string(forColumnIndex: 15)
                         row["lock"] = cursorData.string(forColumnIndex: 16)
                         row["is_stared"] = cursorData.string(forColumnIndex: 17)
-                        row["blog_id"] = cursorData.string(forColumnIndex: 18)
-                        row["credential"] = cursorData.string(forColumnIndex: 19)
+                        row["blog_id"] = cursorData.string(forColumnIndex: 18) ?? ""
+                        row["credential"] = cursorData.string(forColumnIndex: 19) ?? ""
                         row[TypeDataMessage.last_edit] = cursorData.longLongInt(forColumnIndex: 20)
-                        row[TypeDataMessage.gif_id] = cursorData.string(forColumnIndex: 21)
+                        row[TypeDataMessage.gif_id] = cursorData.string(forColumnIndex: 21) ?? ""
                         row[TypeDataMessage.is_forwarded] = Int(cursorData.int(forColumnIndex: 22))
-                        row[TypeDataMessage.spec_file] = cursorData.string(forColumnIndex: 23)
+                        row[TypeDataMessage.spec_file] = cursorData.string(forColumnIndex: 23) ?? ""
+                        row[TypeDataMessage.is_pinned] = cursorData.string(forColumnIndex: 24) ?? ""
                         row["isSelected"] = false
                         if row["credential"] != nil && row["credential"]  as? String ?? "" == "1" {
                             let idMe = User.getMyPin()!
@@ -1079,6 +1087,32 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         updateProgress(data)
     }
     
+    @objc func onUpdatedMessage(notification: NSNotification) {
+        DispatchQueue.main.async {
+            let data:[AnyHashable : Any] = notification.userInfo!
+            let messageId = data["message_id"]  as? String ?? ""
+            let messageIdNotif = data["message_id_notif"]  as? String ?? ""
+            let isPinned = data["is_pinned"]  as? String ?? ""
+            let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == messageId})
+            if idx != nil{
+                self.dataMessages[idx!][TypeDataMessage.is_pinned] = isPinned
+                let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"]  as? String ?? "")
+                let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[idx!]["chat_date"]  as? String ?? ""}).firstIndex(where: { $0["message_id"]  as? String ?? "" == self.dataMessages[idx!]["message_id"]  as? String ?? "" })
+                if row != nil && section != nil  {
+                    DispatchQueue.main.async {
+                        self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
+                    }
+                }
+                let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+                self.pinAllMessages(dataMessages: dataMessagesPin)
+                
+                if !messageIdNotif.isEmpty {
+                    self.appendNewMessage(messageId: messageIdNotif)
+                }
+            }
+        }
+    }
+    
     @objc func onReceiveMessage(notification: NSNotification) {
         DispatchQueue.main.async {
             let data:[AnyHashable : Any] = notification.userInfo!
@@ -3355,6 +3389,130 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
                 self.handleReply(indexPath: indexPath!)
             })
         })
+        var pin: UIAction
+        if (dataMessages[indexPath!.row][TypeDataMessage.is_pinned] as? String ?? "0" == "0") {
+            pin = UIAction(title: "Pin".localized(), image: UIImage(systemName: "pin"), handler: {(_) in
+                if self.removed {
+                    return
+                }
+                if self.isSearching {
+                    self.cancelAction()
+                }
+                var checkDataPinned = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
+                    if checkDataPinned.count == 3 {
+                        let alert = UIAlertController(title: "Replace oldest pin?".localized(),
+                                                      message: "Your pin will replace the oldest one.".localized(),
+                                                      preferredStyle: .alert)
+
+                        alert.addAction(UIAlertAction(title: "Continue", style: .default) { _ in
+                            proceedPinned(replace: true)
+                        })
+
+                        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
+                        })
+                        self.present(alert, animated: true, completion: nil)
+                    } else {
+                        proceedPinned()
+                    }
+                })
+                func proceedPinned(replace: Bool = false) {
+                    if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
+                        DispatchQueue.main.async {
+                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                            banner.show()
+                        }
+                        return
+                    }
+                    if replace {
+                        checkDataPinned.sort {
+                            let firstPinned = Int64($0[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+                            let secondPinned = Int64($1[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+                            return firstPinned < secondPinned
+                        }
+                        self.proceedPinUnpinMessage(checkDataPinned: checkDataPinned[0], isPinned: false) { res1 in
+                            if res1 {
+                                self.proceedPinUnpinMessage(checkDataPinned: dataMessages[indexPath!.row], isPinned: true) { res2 in
+                                    if res2 {
+                                        let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+                                        DispatchQueue.main.async {
+                                            self.pinAllMessages(dataMessages: dataMessagesPin)
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    } else {
+                        self.proceedPinUnpinMessage(checkDataPinned: dataMessages[indexPath!.row], isPinned: true) { res in
+                            if res {
+                                let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+                                DispatchQueue.main.async {
+                                    self.pinAllMessages(dataMessages: dataMessagesPin)
+                                }
+                            }
+                        }
+                    }
+                }
+            })
+        } else {
+            pin = UIAction(title: "Unpin".localized(), image: UIImage(systemName: "pin.slash"), handler: {(_) in
+                if self.removed {
+                    return
+                }
+                if self.isSearching {
+                    self.cancelAction()
+                }
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
+                    self.proceedPinUnpinMessage(checkDataPinned: dataMessages[indexPath!.row], isPinned: false) { res in
+                        if res {
+                            let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+                            DispatchQueue.main.async {
+                                self.pinAllMessages(dataMessages: dataMessagesPin)
+                            }
+                        }
+                    }
+                })
+            })
+        }
+        let replyP = UIAction(title: "Reply Privately".localized(), image: UIImage(systemName: "arrowshape.turn.up.left"), handler: {(_) in
+            if self.removed {
+                return
+            }
+            if self.isSearching {
+                self.cancelAction()
+            }
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
+                let f_pin = dataMessages[indexPath!.row]["f_pin"] as? String ?? ""
+                let message_id = dataMessages[indexPath!.row][TypeDataMessage.message_id] as? String ?? ""
+                if let dataSaved: String = SecureUserDefaults.shared.value(forKey: "new_saved_\(f_pin)") {
+                    let data = dataSaved
+                    if let jsonData = data.data(using: .utf8),
+                       let dataJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
+                        let last_m = dataJson["text"] ?? ""
+                        let data: [String: String] = ["text": last_m, "reffId": message_id]
+                        if let jsonData = try? JSONSerialization.data(withJSONObject: data, options: []),
+                           let jsonString = String(data: jsonData, encoding: .utf8) {
+                            SecureUserDefaults.shared.set(jsonString, forKey: "new_saved_\(f_pin)")
+                        }
+                    }
+                } else {
+                    let data: [String: String] = ["text": "", "reffId": message_id]
+                    if let jsonData = try? JSONSerialization.data(withJSONObject: data, options: []),
+                       let jsonString = String(data: jsonData, encoding: .utf8) {
+                        SecureUserDefaults.shared.set(jsonString, forKey: "new_saved_\(f_pin)")
+                    }
+                }
+                let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
+                editorPersonalVC.hidesBottomBarWhenPushed = true
+                editorPersonalVC.unique_l_pin = f_pin
+                if let nav = self.navigationController {
+                    nav.show(editorPersonalVC, sender: nil)
+                    nav.viewControllers.remove(at: nav.viewControllers.count - 2)
+                }
+            })
+        })
         let forward = UIAction(title: "Forward".localized(), image: UIImage(systemName: "arrowshape.turn.up.right"), handler: {(_) in
             if self.removed {
                 return
@@ -3587,7 +3745,7 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
             Nexilis.addQueueMessage(message: message)
         })
         
-        var children: [UIMenuElement] = [star, reply, copy, delete]
+        var children: [UIMenuElement] = [star, reply, pin, copy, delete]
         var isMore = false
 //        let copyOption = self.copyOption(indexPath: indexPath!)
         let idMe = User.getMyPin() as String?
@@ -3604,17 +3762,20 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
                 children.insert(forward, at: 0)
             }
         } else {
-            if dataMessages[indexPath!.row]["f_pin"]  as? String ?? "" == "-999" {
+            if dataMessages[indexPath!.row]["f_pin"] as? String ?? "" == "-999" {
                 children = [star, reply ,delete]
             }
-            else if !(dataMessages[indexPath!.row]["image_id"]  as? String ?? "").isEmpty || !(dataMessages[indexPath!.row]["video_id"]  as? String ?? "").isEmpty || !(dataMessages[indexPath!.row]["file_id"]  as? String ?? "").isEmpty {
-                children = [star, reply ,delete]
+            else if !(dataMessages[indexPath!.row]["image_id"] as? String ?? "").isEmpty || !(dataMessages[indexPath!.row]["video_id"] as? String ?? "").isEmpty || !(dataMessages[indexPath!.row]["file_id"] as? String ?? "").isEmpty {
+                children = [star, reply, pin, delete]
             } else if dataMessages[indexPath!.row]["attachment_flag"]  as? String ?? "" == "11" {
-                children = [reply, delete]
+                children = [reply, pin, delete]
             }
             if (Nexilis.checkingAccess(key: "secure_folder_forward") && dataMessages[indexPath!.row]["attachment_flag"]  as? String ?? "" != "11") || (!(dataMessages[indexPath!.row][TypeDataMessage.message_text]  as? String ?? "").isEmpty && (dataMessages[indexPath!.row]["image_id"]  as? String ?? "").isEmpty && (dataMessages[indexPath!.row]["video_id"]  as? String ?? "").isEmpty && (dataMessages[indexPath!.row]["file_id"]  as? String ?? "").isEmpty && (dataMessages[indexPath!.row]["audio_id"]  as? String ?? "").isEmpty && dataMessages[indexPath!.row]["read_receipts"] as? String != "8") || (dataMessages[indexPath!.row][TypeDataMessage.spec_file] as? String ?? "").contains("forward") {
                 children.insert(forward, at: 2)
             }
+            if dataMessages[indexPath!.row]["f_pin"] as? String ?? "" != "-999" && dataMessages[indexPath!.row]["f_pin"] as? String != User.getMyPin() && dataMessages[indexPath!.row]["attachment_flag"]  as? String ?? "" != "11" {
+                children.insert(replyP, at: 2)
+            }
             if (dataMessages[indexPath!.row]["f_pin"]  as? String ?? "") == idMe {
                 children.insert(info, at: children.count - 1)
             }
@@ -3648,6 +3809,117 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
         }
     }
     
+    func proceedPinUnpinMessage(checkDataPinned: [String: Any?], isPinned: Bool, completion: @escaping (Bool)-> Void) {
+        DispatchQueue.global().async {
+            var jaData = [[String: Any]]()
+            var jsonObject = [String: Any]()
+            jsonObject[CoreMessage_TMessageKey.MESSAGE_ID] = checkDataPinned["message_id"]  as? String ?? ""
+            jsonObject[CoreMessage_TMessageKey.IS_PINNED_MESSAGE] = isPinned ? "\(Date().currentTimeMillis())" : "0"
+            jaData.append(jsonObject)
+            if let jsonData = try? JSONSerialization.data(withJSONObject: jaData, options: []),
+               let jsonString = String(data: jsonData, encoding: .utf8) {
+                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getPinMessage(f_pin: User.getMyPin() ?? "", data: jsonString, oppositePin: self.unique_l_pin, chatId: self.dataTopic["chat_id"] as? String ?? "", scopeId: MessageScope.GROUP)) {
+                    if response.isOk() {
+                        if isPinned {
+                            let mId = Nexilis.saveMessageNotif(textMessage: "You".localized() + " " + "pinned a message".localized(), fPin: User.getMyPin() ?? "", lPin: self.unique_l_pin, chatId: self.dataTopic["chat_id"] as? String ?? "", scopeId: MessageScope.GROUP)
+                            self.appendNewMessage(messageId: mId)
+                        }
+                        DispatchQueue.global().async {
+                            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                do {
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
+                                        "is_pinned" : isPinned ? "\(Date().currentTimeMillis())" : "0"
+                                    ], _where: "message_id = '\(checkDataPinned["message_id"]  as? String ?? "")'")
+                                } catch {
+                                    rollback.pointee = true
+                                    print("Access database error: \(error.localizedDescription)")
+                                }
+                            })
+                        }
+                        let idx = self.dataMessages.firstIndex(where: { $0["message_id"]  as? String ?? "" == checkDataPinned["message_id"]  as? String ?? ""})
+                        if idx != nil{
+                            self.dataMessages[idx!][TypeDataMessage.is_pinned] = isPinned ? "\(Date().currentTimeMillis())" : "0"
+                            let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"]  as? String ?? "")
+                            let row = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == self.dataMessages[idx!]["chat_date"]  as? String ?? ""}).firstIndex(where: { $0["message_id"]  as? String ?? "" == self.dataMessages[idx!]["message_id"]  as? String ?? "" })
+                            if row != nil && section != nil  {
+                                DispatchQueue.main.async {
+                                    self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
+                                }
+                            }
+                        }
+                        completion(true)
+                    } else {
+                        DispatchQueue.main.async {
+                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Failed to pin or unpin message, make sure you are connected to internet".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                            banner.show()
+                        }
+                        completion(false)
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                        banner.show()
+                    }
+                    completion(false)
+                }
+            }
+        }
+    }
+    
+    private func appendNewMessage(messageId: String) {
+        var row: [String: Any?] = [:]
+        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+            if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, is_call_center, call_center_id, opposite_pin, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned from MESSAGE where message_id = '\(messageId)'"), cursorData.next() {
+                row["message_id"] = cursorData.string(forColumnIndex: 0)
+                row["f_pin"] = cursorData.string(forColumnIndex: 1)
+                row["l_pin"] = cursorData.string(forColumnIndex: 2)
+                row["message_scope_id"] = cursorData.string(forColumnIndex: 3)
+                row["server_date"] = cursorData.string(forColumnIndex: 4)
+                row["status"] = cursorData.string(forColumnIndex: 5)
+                row["message_text"] = cursorData.string(forColumnIndex: 6)
+                row["audio_id"] = cursorData.string(forColumnIndex: 7)
+                row["video_id"] = cursorData.string(forColumnIndex: 8)
+                row["image_id"] = cursorData.string(forColumnIndex: 9)
+                row["thumb_id"] = cursorData.string(forColumnIndex: 10)
+                row["read_receipts"] = cursorData.string(forColumnIndex: 11)
+                row["chat_id"] = cursorData.string(forColumnIndex: 12)
+                row["file_id"] = cursorData.string(forColumnIndex: 13)
+                row["attachment_flag"] = cursorData.string(forColumnIndex: 14)
+                row["reff_id"] = cursorData.string(forColumnIndex: 15)
+                row["lock"] = cursorData.string(forColumnIndex: 16)
+                row["is_stared"] = cursorData.string(forColumnIndex: 17)
+                row["blog_id"] = cursorData.string(forColumnIndex: 18)
+                row["credential"] = cursorData.string(forColumnIndex: 19)
+                row[TypeDataMessage.is_call_center] = cursorData.string(forColumnIndex: 20)
+                row[TypeDataMessage.call_center_id] = cursorData.string(forColumnIndex: 21)
+                row[TypeDataMessage.opposite_pin] = cursorData.string(forColumnIndex: 22)
+                row[TypeDataMessage.last_edit] = cursorData.longLongInt(forColumnIndex: 23)
+                row[TypeDataMessage.gif_id] = cursorData.string(forColumnIndex: 24)
+                row[TypeDataMessage.is_forwarded] = Int(cursorData.int(forColumnIndex: 25))
+                row[TypeDataMessage.spec_file] = cursorData.string(forColumnIndex: 26)
+                row[TypeDataMessage.is_pinned] = cursorData.string(forColumnIndex: 27)
+                row["progress"] = 0.0
+                row["isSelected"] = false
+                if !self.dataDates.contains("Today".localized()) {
+                    self.dataDates.append("Today".localized())
+                    self.tableChatView.insertSections(IndexSet(integer: self.dataDates.count - 1), with: .none)
+                }
+                row["chat_date"] = "Today".localized()
+                cursorData.close()
+            }
+        })
+        DispatchQueue.main.async {
+            self.tableChatView.beginUpdates()
+            self.dataMessages.append(row)
+            self.tableChatView.insertRows(at: [IndexPath(row: self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == self.dataDates[self.dataDates.count - 1]}).count - 1, section: self.dataDates.count - 1)], with: .none)
+            self.tableChatView.endUpdates()
+        }
+    }
+    
     func showEditMessageView(at indexPath: IndexPath) {
         tempListMentionWithText = listMentionWithText
         tempListMentionInTextField = listMentionInTextField
@@ -4793,6 +5065,22 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
         cellMessage.contentView.addSubview(containerMessage)
         containerMessage.translatesAutoresizingMaskIntoConstraints = false
         
+        if messageIdChat.contains("NTFPIN_") {
+            containerMessage.backgroundColor = .orangeColor
+            containerMessage.anchor(top: cellMessage.contentView.topAnchor, bottom: cellMessage.contentView.bottomAnchor, paddingTop: 5, paddingBottom: 5, centerX: cellMessage.contentView.centerXAnchor, minWidth: 40, maxWidth: UIScreen.main.bounds.width - 40)
+            containerMessage.layer.cornerRadius = 15
+            containerMessage.clipsToBounds = true
+            
+            let textMessage = UILabel()
+            containerMessage.addSubview(textMessage)
+            textMessage.textAlignment = .center
+            textMessage.anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, right: containerMessage.rightAnchor, paddingTop: 10, paddingLeft: 10, paddingBottom: 10, paddingRight: 10)
+            textMessage.font = .systemFont(ofSize: 14)
+            textMessage.text = dataMessages[indexPath.row][TypeDataMessage.message_text]  as? String ?? ""
+            textMessage.textColor = .white
+            return cellMessage
+        }
+        
         let timeMessage = UILabel()
         timeMessage.numberOfLines = 0
         cellMessage.contentView.addSubview(timeMessage)
@@ -5062,6 +5350,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
         let imageStared = UIImageView()
         let imageAckView = UIImageView()
         let imageCredentialView = UIImageView()
+        let imagePinView = UIImageView()
         if dataMessages[indexPath.row]["is_stared"] as? String == "1" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"]  as? String ?? "" == "0") {
             cellMessage.contentView.addSubview(imageStared)
             imageStared.translatesAutoresizingMaskIntoConstraints = false
@@ -5079,6 +5368,31 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
             imageStared.tintColor = .systemYellow
         }
         
+        if dataMessages[indexPath.row][TypeDataMessage.is_pinned] as? String != nil && dataMessages[indexPath.row][TypeDataMessage.is_pinned] as? String != "0" {
+            cellMessage.contentView.addSubview(imagePinView)
+            imagePinView.translatesAutoresizingMaskIntoConstraints = false
+            if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+                if imageStared.isDescendant(of: cellMessage.contentView){
+                    imagePinView.bottomAnchor.constraint(equalTo: imageStared.topAnchor).isActive = true
+                } else {
+                    imagePinView.bottomAnchor.constraint(equalTo: statusMessage.topAnchor).isActive = true
+                }
+                imagePinView.trailingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: -8).isActive = true
+            } else {
+                if imageStared.isDescendant(of: cellMessage.contentView){
+                    imagePinView.bottomAnchor.constraint(equalTo: imageStared.topAnchor).isActive = true
+                } else {
+                    imagePinView.bottomAnchor.constraint(equalTo: timeMessage.topAnchor).isActive = true
+                }
+                imagePinView.leadingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: 8).isActive = true
+            }
+            imagePinView.widthAnchor.constraint(equalToConstant: 15).isActive = true
+            imagePinView.heightAnchor.constraint(equalToConstant: 15).isActive = true
+            imagePinView.image = UIImage(systemName: "pin.fill")
+            imagePinView.backgroundColor = .clear
+            imagePinView.tintColor = .lightGray
+        }
+        
         if dataMessages[indexPath.row]["read_receipts"] as? String == "8"  && (dataMessages[indexPath.row]["lock"] as? String) != "2" && (dataMessages[indexPath.row]["lock"] as? String) != "1" {
             var imageAck = UIImage(named: "ack_icon_gray", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal)
             cellMessage.contentView.addSubview(imageAckView)
@@ -6933,6 +7247,174 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
     //        return UISwipeActionsConfiguration(actions: [action])
     //    }
     
+    private func pinAllMessages(dataMessages: [[String: Any?]], isPinned: Int = -1) {
+        var dataMessages = dataMessages
+        dataMessages.sort {
+            let firstPinned = Int64($0[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+            let secondPinned = Int64($1[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+            return firstPinned < secondPinned
+        }
+        if dataMessages.count != 0 {
+            if !self.containerPin.isDescendant(of: self.view) && dataMessages.count != 0 {
+                self.tableChatView.contentInset.top = 50
+                
+                self.view.addSubview(self.containerPin)
+                self.containerPin.isUserInteractionEnabled = true
+                let tapGesture = UITapGestureRecognizer(target: self, action: #selector(viewPinTapped))
+                self.containerPin.addGestureRecognizer(tapGesture)
+                self.containerPin.anchor(top: self.view.safeAreaLayoutGuide.topAnchor, left: self.view.leftAnchor, right: self.view.rightAnchor, height: 50)
+                self.containerPin.backgroundColor = .mainColor
+                
+                if dataMessages.count > 1 {
+                    self.containerPin.addSubview(self.signSelectedPin)
+                    self.signSelectedPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 8, centerY: self.containerPin.centerYAnchor, width: 2, height: 30)
+                    self.signSelectedPin.layer.cornerRadius = 1
+                    self.signSelectedPin.clipsToBounds = true
+                    self.signSelectedPin.alignment = .fill
+                    self.signSelectedPin.axis = .vertical
+                    self.signSelectedPin.distribution = .fill
+                    self.signSelectedPin.spacing = dataMessages.count == 3 ? 1.5 : 2
+                    
+                    let heightSign: CGFloat = CGFloat((30 / dataMessages.count) - 1)
+                    let widthSign: CGFloat = 2
+
+                    for i in 0..<dataMessages.count {
+                        let viewSign = UIView()
+                        viewSign.backgroundColor = (i == (dataMessages.count - 1)) ? .white : .gray
+                        viewSign.anchor(width: widthSign, height: heightSign)
+                        viewSign.layer.cornerRadius = 1
+                        viewSign.clipsToBounds = true
+                        self.signSelectedPin.addArrangedSubview(viewSign)
+                    }
+                    self.nextPinShowed = dataMessages.count - 1
+                }
+                
+                let contIconPin = UIImageView()
+                self.containerPin.addSubview(contIconPin)
+                contIconPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 15, centerY: self.containerPin.centerYAnchor, width: 30, height: 30)
+                contIconPin.layer.cornerRadius = 8
+                contIconPin.clipsToBounds = true
+                contIconPin.backgroundColor = .gray
+                contIconPin.image = UIImage(systemName: "pin.fill")?.imageWithInsets(insets: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5))?.withTintColor(.waGrayLight)
+                
+                self.containerPin.addSubview(textPin)
+                self.textPin.anchor(left: contIconPin.rightAnchor, right: self.containerPin.rightAnchor, paddingLeft: 10, paddingRight: 10, centerY: self.containerPin.centerYAnchor)
+                self.textPin.attributedText = (dataMessages[dataMessages.count - 1][TypeDataMessage.message_text] as? String ?? "").richText(fontSize: 14, group_id: self.dataGroup["group_id"]  as? String ?? "")
+                self.textPin.numberOfLines = 1
+                self.textPin.textColor = .white
+            } else {
+                self.signSelectedPin.subviews.forEach({ $0.removeFromSuperview() })
+                self.signSelectedPin.removeFromSuperview()
+                var same = false
+                if dataMessages.count > 1 {
+                    self.containerPin.addSubview(self.signSelectedPin)
+                    self.signSelectedPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 8, centerY: self.containerPin.centerYAnchor, width: 2, height: 30)
+                    self.signSelectedPin.layer.cornerRadius = 1
+                    self.signSelectedPin.clipsToBounds = true
+                    self.signSelectedPin.alignment = .fill
+                    self.signSelectedPin.axis = .vertical
+                    self.signSelectedPin.distribution = .fill
+                    self.signSelectedPin.spacing = dataMessages.count == 3 ? 1.5 : 2
+                    
+                    let heightSign: CGFloat = CGFloat((30 / dataMessages.count) - 1)
+                    let widthSign: CGFloat = 2
+
+                    for i in 0..<dataMessages.count {
+                        let viewSign = UIView()
+                        viewSign.backgroundColor = (i == (dataMessages.count - 1)) ? .white : .gray
+                        viewSign.anchor(width: widthSign, height: heightSign)
+                        viewSign.layer.cornerRadius = 1
+                        viewSign.clipsToBounds = true
+                        self.signSelectedPin.addArrangedSubview(viewSign)
+                    }
+                    if isPinned == -1 {
+                        self.nextPinShowed = dataMessages.count - 1
+                    } else if self.nextPinShowed != 0 {
+                        if (self.nextPinShowed > isPinned) {
+                            self.nextPinShowed-=1
+                            same = true
+                        } else if self.nextPinShowed == isPinned && dataMessages.count == 3 {
+                            self.nextPinShowed-=2
+                        }
+                    } else if self.nextPinShowed != isPinned {
+                        same = true
+                    }
+                } else if self.nextPinShowed != isPinned {
+                    same = true
+                }
+                if !same{
+                    animateLabelTextChange(label: self.textPin, newText: dataMessages[dataMessages.count - 1][TypeDataMessage.message_text] as? String ?? "")
+                }
+            }
+        } else if self.containerPin.isDescendant(of: self.view) {
+            self.containerPin.subviews.forEach({ $0.removeFromSuperview() })
+            self.containerPin.removeFromSuperview()
+            self.tableChatView.contentInset.top = 0
+        }
+    }
+    
+    @objc func viewPinTapped() {
+        var dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+        dataMessagesPin.sort {
+            let firstPinned = Int64($0[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+            let secondPinned = Int64($1[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+            return firstPinned < secondPinned
+        }
+        let obj = ObjectGesture()
+        obj.message_id = dataMessagesPin[nextPinShowed][TypeDataMessage.message_id] as? String ?? ""
+        contentMessageTapped(obj)
+        
+        if nextPinShowed < dataMessagesPin.count - 1 {
+            nextPinShowed+=1
+        } else {
+            nextPinShowed = 0
+        }
+        
+        DispatchQueue.main.async {
+            self.signSelectedPin.subviews.forEach({ $0.removeFromSuperview() })
+            self.signSelectedPin.removeFromSuperview()
+            self.containerPin.addSubview(self.signSelectedPin)
+            self.signSelectedPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 8, centerY: self.containerPin.centerYAnchor, width: 2, height: 30)
+            self.signSelectedPin.layer.cornerRadius = 1
+            self.signSelectedPin.clipsToBounds = true
+            self.signSelectedPin.alignment = .fill
+            self.signSelectedPin.axis = .vertical
+            self.signSelectedPin.distribution = .fill
+            self.signSelectedPin.spacing = dataMessagesPin.count == 3 ? 1.5 : 2
+            
+            let heightSign: CGFloat = CGFloat((30 / dataMessagesPin.count) - 1)
+            let widthSign: CGFloat = 2
+
+            for i in 0..<dataMessagesPin.count {
+                let viewSign = UIView()
+                viewSign.backgroundColor = (i == self.nextPinShowed) ? .white : .gray
+                viewSign.anchor(width: widthSign, height: heightSign)
+                viewSign.layer.cornerRadius = 1
+                viewSign.clipsToBounds = true
+                self.signSelectedPin.addArrangedSubview(viewSign)
+            }
+            self.animateLabelTextChange(label: self.textPin, newText: dataMessagesPin[self.nextPinShowed][TypeDataMessage.message_text] as? String ?? "")
+        }
+    }
+    
+    func animateLabelTextChange(label: UILabel, newText: String) {
+        let animationDuration = 0.1
+        UIView.animate(withDuration: animationDuration, animations: {
+            label.transform = CGAffineTransform(translationX: 0, y: -10)
+            label.alpha = 0
+        }) { _ in
+            // Change text after fade out
+            label.attributedText = newText.richText(fontSize: 14, group_id: self.dataGroup["group_id"]  as? String ?? "")
+            label.transform = CGAffineTransform(translationX: 0, y: 10)
+            
+            // Animate back to original position and fade in
+            UIView.animate(withDuration: animationDuration) {
+                label.transform = .identity
+                label.alpha = 1
+            }
+        }
+    }
+    
     private func handleReply(indexPath: IndexPath, dataMessagesImage: [String: Any?] = [:], reffId: String = "") {
         var dataMessages = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[indexPath.section]})
         if reffId.isEmpty {

+ 483 - 47
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -78,6 +78,9 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     let viewSticker = UIView()
     let containerLink = UIView()
     let containerPreviewReply = UIView()
+    let containerPin = UIView()
+    let textPin = UILabel()
+    let signSelectedPin = UIStackView()
     var bottomAnchorPreviewReply = NSLayoutConstraint()
     var blocking = ""
     var timeoutCC = Timer()
@@ -93,6 +96,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     var constraintBottomContainerMultpileSelectSession = NSLayoutConstraint()
     var titleSearchMatches: UILabel!
     var textSearch = ""
+    var nextPinShowed = 0
     var countMatchesSearch = 0
     var lastScrollIdxSearch = 0
     var buttonUp: UIButton!
@@ -277,6 +281,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         center.addObserver(self, selector: #selector(onTyping(notification:)), name: NSNotification.Name(rawValue: Nexilis.listenerTypingChat), object: nil)
         center.addObserver(self, selector: #selector(onFailedSendMessage(notification:)), name: NSNotification.Name(rawValue: Nexilis.failedSendMessage), object: nil)
         center.addObserver(self, selector: #selector(onRefreshCallLog(notification:)), name: NSNotification.Name(rawValue: "refreshCallLog"), object: nil)
+        center.addObserver(self, selector: #selector(onUpdatedMessage(notification:)), name: NSNotification.Name(rawValue: "onUpdatedMessage"), object: nil)
         
         locationManager.delegate = self
         locationManager.requestWhenInUseAuthorization()
@@ -727,6 +732,8 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                 })
             }
         }
+        let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+        pinAllMessages(dataMessages: dataMessagesPin)
     }
     
     private func chatbot() {
@@ -955,7 +962,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     private func addDataMessage() {
         multipleOffsetUp += 1
         let queryCount = "SELECT COUNT(*) FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '\(MessageScope.WHISPER)' OR message_scope_id = '\(MessageScope.FORM)' OR message_scope_id = '\(MessageScope.CALL)' OR message_scope_id = '\(MessageScope.MISSED_CALL)') AND is_call_center = 0"
-        let query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, is_call_center, call_center_id, opposite_pin, last_edited, gif_id, is_forwarded_message, attachment_speciality FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '\(MessageScope.WHISPER)' OR message_scope_id = '\(MessageScope.FORM)' OR message_scope_id = '\(MessageScope.CALL)' OR message_scope_id = '\(MessageScope.MISSED_CALL)') AND is_call_center = 0 order by server_date asc LIMIT CASE WHEN (\(queryCount))-\(dataMessages.count)>=20 THEN 20*\(multipleOffsetUp-1) ELSE (\(queryCount))-\(dataMessages.count) END OFFSET CASE WHEN (\(queryCount))>=\(20*multipleOffsetUp) THEN (\(queryCount))-\(20*multipleOffsetUp) ELSE 0 END"
+        let query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, is_call_center, call_center_id, opposite_pin, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '\(MessageScope.WHISPER)' OR message_scope_id = '\(MessageScope.FORM)' OR message_scope_id = '\(MessageScope.CALL)' OR message_scope_id = '\(MessageScope.MISSED_CALL)') AND is_call_center = 0 order by server_date asc LIMIT CASE WHEN (\(queryCount))-\(dataMessages.count)>=20 THEN 20*\(multipleOffsetUp-1) ELSE (\(queryCount))-\(dataMessages.count) END OFFSET CASE WHEN (\(queryCount))>=\(20*multipleOffsetUp) THEN (\(queryCount))-\(20*multipleOffsetUp) ELSE 0 END"
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
                 if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
@@ -989,6 +996,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                         row[TypeDataMessage.gif_id] = cursorData.string(forColumnIndex: 24)
                         row[TypeDataMessage.is_forwarded] = Int(cursorData.int(forColumnIndex: 25))
                         row[TypeDataMessage.spec_file] = cursorData.string(forColumnIndex: 26)
+                        row[TypeDataMessage.is_pinned] = cursorData.string(forColumnIndex: 27)
                         if let cursorStatus = Database.shared.getRecords(fmdb: fmdb, query: "SELECT status FROM MESSAGE_STATUS WHERE message_id='\(row["message_id"]  as? String ?? "")'") {
                             while cursorStatus.next() {
                                 row["status"] = cursorStatus.string(forColumnIndex: 0)
@@ -1100,7 +1108,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     private func getData() {
 //        let queryCount = "SELECT COUNT(*) FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '3' OR message_scope_id = '18') AND is_call_center = 0"
 //        var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '3' OR message_scope_id = '18') AND is_call_center = 0 order by server_date asc LIMIT CASE WHEN (\(queryCount))-\(dataMessages.count)>=20 THEN 20 ELSE (\(queryCount))-\(dataMessages.count) END OFFSET CASE WHEN (\(queryCount))>=\(20*multipleOffsetUp) THEN (\(queryCount))-\(20*multipleOffsetUp) ELSE 0 END"
-        var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, is_call_center, call_center_id, opposite_pin, last_edited, gif_id, is_forwarded_message, attachment_speciality FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '\(MessageScope.WHISPER)' OR message_scope_id = '\(MessageScope.FORM)' OR message_scope_id = '\(MessageScope.CALL)' OR message_scope_id = '\(MessageScope.MISSED_CALL)') AND is_call_center = 0 order by server_date asc"
+        var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, is_call_center, call_center_id, opposite_pin, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND (message_scope_id = '\(MessageScope.WHISPER)' OR message_scope_id = '\(MessageScope.FORM)' OR message_scope_id = '\(MessageScope.CALL)' OR message_scope_id = '\(MessageScope.MISSED_CALL)') AND is_call_center = 0 order by server_date asc"
         if isContactCenter {
             if complaintId.isEmpty {
                 query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared FROM MESSAGE where (f_pin='\(dataPerson["f_pin"]!!)' or l_pin='\(dataPerson["f_pin"]!!)') AND message_scope_id = '\(MessageScope.CHATROOM)' AND broadcast_flag = 0 AND is_call_center = 1 order by server_date asc"
@@ -1159,15 +1167,16 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                         row["reff_id"] = cursorData.string(forColumnIndex: 15)
                         row["lock"] = cursorData.string(forColumnIndex: 16)
                         row["is_stared"] = cursorData.string(forColumnIndex: 17)
-                        row["blog_id"] = cursorData.string(forColumnIndex: 18)
-                        row["credential"] = cursorData.string(forColumnIndex: 19)
-                        row[TypeDataMessage.is_call_center] = cursorData.string(forColumnIndex: 20)
-                        row[TypeDataMessage.call_center_id] = cursorData.string(forColumnIndex: 21)
-                        row[TypeDataMessage.opposite_pin] = cursorData.string(forColumnIndex: 22)
+                        row["blog_id"] = cursorData.string(forColumnIndex: 18) ?? ""
+                        row["credential"] = cursorData.string(forColumnIndex: 19) ?? ""
+                        row[TypeDataMessage.is_call_center] = cursorData.string(forColumnIndex: 20) ?? ""
+                        row[TypeDataMessage.call_center_id] = cursorData.string(forColumnIndex: 21) ?? ""
+                        row[TypeDataMessage.opposite_pin] = cursorData.string(forColumnIndex: 22) ?? ""
                         row[TypeDataMessage.last_edit] = cursorData.longLongInt(forColumnIndex: 23)
-                        row[TypeDataMessage.gif_id] = cursorData.string(forColumnIndex: 24)
+                        row[TypeDataMessage.gif_id] = cursorData.string(forColumnIndex: 24) ?? ""
                         row[TypeDataMessage.is_forwarded] = Int(cursorData.int(forColumnIndex: 25))
-                        row[TypeDataMessage.spec_file] = cursorData.string(forColumnIndex: 26)
+                        row[TypeDataMessage.spec_file] = cursorData.string(forColumnIndex: 26) ?? ""
+                        row[TypeDataMessage.is_pinned] = cursorData.string(forColumnIndex: 27) ?? ""
                         if let cursorStatus = Database.shared.getRecords(fmdb: fmdb, query: "SELECT status FROM MESSAGE_STATUS WHERE message_id='\(row["message_id"] as? String ?? "")'") {
                             while cursorStatus.next() {
                                 row["status"] = cursorStatus.string(forColumnIndex: 0)
@@ -1461,6 +1470,32 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         updateProgress(data)
     }
     
+    @objc func onUpdatedMessage(notification: NSNotification) {
+        DispatchQueue.main.async {
+            let data:[AnyHashable : Any] = notification.userInfo!
+            let messageId = data["message_id"]  as? String ?? ""
+            let messageIdNotif = data["message_id_notif"]  as? String ?? ""
+            let isPinned = data["is_pinned"]  as? String ?? ""
+            let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == messageId})
+            if idx != nil{
+                self.dataMessages[idx!][TypeDataMessage.is_pinned] = isPinned
+                let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"]  as? String ?? "")
+                let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[idx!]["chat_date"]  as? String ?? ""}).firstIndex(where: { $0["message_id"]  as? String ?? "" == self.dataMessages[idx!]["message_id"]  as? String ?? "" })
+                if row != nil && section != nil  {
+                    DispatchQueue.main.async {
+                        self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
+                    }
+                }
+                let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+                self.pinAllMessages(dataMessages: dataMessagesPin)
+                
+                if !messageIdNotif.isEmpty {
+                    self.appendNewMessage(messageId: messageIdNotif)
+                }
+            }
+        }
+    }
+    
     
     @objc func onReceiveMessage(notification: NSNotification) {
         DispatchQueue.main.async { [self] in
@@ -1939,9 +1974,9 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     }
     
     private func appendNewMessage(messageId: String) {
+        var row: [String: Any?] = [:]
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
-            if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, is_call_center, call_center_id, opposite_pin, last_edited, gif_id, is_forwarded_message, attachment_speciality from MESSAGE where message_id = '\(messageId)'"), cursorData.next() {
-                var row: [String: Any?] = [:]
+            if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, is_call_center, call_center_id, opposite_pin, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned from MESSAGE where message_id = '\(messageId)'"), cursorData.next() {
                 row["message_id"] = cursorData.string(forColumnIndex: 0)
                 row["f_pin"] = cursorData.string(forColumnIndex: 1)
                 row["l_pin"] = cursorData.string(forColumnIndex: 2)
@@ -1969,6 +2004,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                 row[TypeDataMessage.gif_id] = cursorData.string(forColumnIndex: 24)
                 row[TypeDataMessage.is_forwarded] = Int(cursorData.int(forColumnIndex: 25))
                 row[TypeDataMessage.spec_file] = cursorData.string(forColumnIndex: 26)
+                row[TypeDataMessage.is_pinned] = cursorData.string(forColumnIndex: 27)
                 row["progress"] = 0.0
                 row["isSelected"] = false
                 if !self.dataDates.contains("Today".localized()) {
@@ -1976,13 +2012,15 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                     self.tableChatView.insertSections(IndexSet(integer: self.dataDates.count - 1), with: .none)
                 }
                 row["chat_date"] = "Today".localized()
-                self.tableChatView.beginUpdates()
-                dataMessages.append(row)
-                self.tableChatView.insertRows(at: [IndexPath(row: self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == self.dataDates[self.dataDates.count - 1]}).count - 1, section: self.dataDates.count - 1)], with: .none)
-                self.tableChatView.endUpdates()
                 cursorData.close()
             }
         })
+        DispatchQueue.main.async {
+            self.tableChatView.beginUpdates()
+            self.dataMessages.append(row)
+            self.tableChatView.insertRows(at: [IndexPath(row: self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == self.dataDates[self.dataDates.count - 1]}).count - 1, section: self.dataDates.count - 1)], with: .none)
+            self.tableChatView.endUpdates()
+        }
     }
     
     private func updateStatusDelete(idx: Int?, chatData: [String: String]) {
@@ -4525,6 +4563,100 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
                 self.handleReply(indexPath: indexPath!)
             })
         })
+        var pin: UIAction
+        if (dataMessages[indexPath!.row][TypeDataMessage.is_pinned] as? String ?? "0" == "0") {
+            pin = UIAction(title: "Pin".localized(), image: UIImage(systemName: "pin"), handler: {(_) in
+                if self.removed {
+                    return
+                }
+                if self.isSearching {
+                    self.cancelAction()
+                }
+                var checkDataPinned = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
+                    if checkDataPinned.count == 3 {
+                        let alert = UIAlertController(title: "Replace oldest pin?".localized(),
+                                                      message: "Your pin will replace the oldest one.".localized(),
+                                                      preferredStyle: .alert)
+
+                        alert.addAction(UIAlertAction(title: "Continue", style: .default) { _ in
+                            proceedPinned(replace: true)
+                        })
+
+                        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
+                        })
+                        self.present(alert, animated: true, completion: nil)
+                    } else {
+                        proceedPinned()
+                    }
+                })
+                func proceedPinned(replace: Bool = false) {
+                    if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
+                        DispatchQueue.main.async {
+                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                            banner.show()
+                        }
+                        return
+                    }
+                    if replace {
+                        checkDataPinned.sort {
+                            let firstPinned = Int64($0[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+                            let secondPinned = Int64($1[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+                            return firstPinned < secondPinned
+                        }
+                        self.proceedPinUnpinMessage(checkDataPinned: checkDataPinned[0], isPinned: false) { res1 in
+                            if res1 {
+                                self.proceedPinUnpinMessage(checkDataPinned: dataMessages[indexPath!.row], isPinned: true) { res2 in
+                                    if res2 {
+                                        let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+                                        DispatchQueue.main.async {
+                                            self.pinAllMessages(dataMessages: dataMessagesPin)
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    } else {
+                        self.proceedPinUnpinMessage(checkDataPinned: dataMessages[indexPath!.row], isPinned: true) { res in
+                            if res {
+                                let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+                                DispatchQueue.main.async {
+                                    self.pinAllMessages(dataMessages: dataMessagesPin)
+                                }
+                            }
+                        }
+                    }
+                }
+            })
+        } else {
+            pin = UIAction(title: "Unpin".localized(), image: UIImage(systemName: "pin.slash"), handler: {(_) in
+                if self.removed {
+                    return
+                }
+                if self.isSearching {
+                    self.cancelAction()
+                }
+                var checkDataPinned = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+                checkDataPinned.sort {
+                    let firstPinned = Int64($0[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+                    let secondPinned = Int64($1[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+                    return firstPinned < secondPinned
+                }
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
+                    let indexUnpinned = checkDataPinned.firstIndex(where: { $0[TypeDataMessage.message_id] as? String == dataMessages[indexPath!.row][TypeDataMessage.message_id] as? String })
+                    self.proceedPinUnpinMessage(checkDataPinned: dataMessages[indexPath!.row], isPinned: false) { res in
+                        if res {
+                            let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+                            DispatchQueue.main.async {
+                                self.pinAllMessages(dataMessages: dataMessagesPin, isPinned: indexUnpinned ?? 0)
+                            }
+                        }
+                    }
+                })
+            })
+        }
         let forward = UIAction(title: "Forward".localized(), image: UIImage(systemName: "arrowshape.turn.up.right"), handler: {(_) in
             if self.removed {
                 return
@@ -4768,7 +4900,7 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
             Nexilis.addQueueMessage(message: message)
         })
         
-        var children: [UIMenuElement] = [star, reply, copy, delete]
+        var children: [UIMenuElement] = [star, reply, pin, copy, delete]
         var isMore = false
         let idMe = User.getMyPin() as String?
         if dataMessages[indexPath!.row]["status"]  as? String ?? "" == "0" {
@@ -4791,7 +4923,7 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
             }
         } else {
             if !(dataMessages[indexPath!.row]["image_id"]  as? String ?? "").isEmpty || !(dataMessages[indexPath!.row]["video_id"]  as? String ?? "").isEmpty || !(dataMessages[indexPath!.row]["file_id"]  as? String ?? "").isEmpty {
-               children = [star, reply ,delete]
+               children = [star, reply , pin, delete]
             } else if dataMessages[indexPath!.row]["attachment_flag"]  as? String ?? "" == "11" {
                children = [reply, delete]
             }
@@ -4825,6 +4957,67 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
         }
     }
     
+    func proceedPinUnpinMessage(checkDataPinned: [String: Any?], isPinned: Bool, completion: @escaping (Bool)-> Void) {
+        DispatchQueue.global().async {
+            var jaData = [[String: Any]]()
+            var jsonObject = [String: Any]()
+            jsonObject[CoreMessage_TMessageKey.MESSAGE_ID] = checkDataPinned["message_id"]  as? String ?? ""
+            jsonObject[CoreMessage_TMessageKey.IS_PINNED_MESSAGE] = isPinned ? "\(Date().currentTimeMillis())" : "0"
+            jaData.append(jsonObject)
+            if let jsonData = try? JSONSerialization.data(withJSONObject: jaData, options: []),
+               let jsonString = String(data: jsonData, encoding: .utf8) {
+                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getPinMessage(f_pin: User.getMyPin() ?? "", data: jsonString, oppositePin: self.unique_l_pin, chatId: "", scopeId: MessageScope.WHISPER)) {
+                    if response.isOk() {
+                        if isPinned {
+                            let mId = Nexilis.saveMessageNotif(textMessage: "You".localized() + " " + "pinned a message".localized(), fPin: User.getMyPin() ?? "", lPin: self.unique_l_pin, chatId: "", scopeId: MessageScope.WHISPER)
+                            self.appendNewMessage(messageId: mId)
+                        }
+                        DispatchQueue.global().async {
+                            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                do {
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
+                                        "is_pinned" : isPinned ? "\(Date().currentTimeMillis())" : "0"
+                                    ], _where: "message_id = '\(checkDataPinned["message_id"]  as? String ?? "")'")
+                                } catch {
+                                    rollback.pointee = true
+                                    print("Access database error: \(error.localizedDescription)")
+                                }
+                            })
+                        }
+                        let idx = self.dataMessages.firstIndex(where: { $0["message_id"]  as? String ?? "" == checkDataPinned["message_id"]  as? String ?? ""})
+                        if idx != nil{
+                            self.dataMessages[idx!][TypeDataMessage.is_pinned] = isPinned ? "\(Date().currentTimeMillis())" : "0"
+                            let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"]  as? String ?? "")
+                            let row = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == self.dataMessages[idx!]["chat_date"]  as? String ?? ""}).firstIndex(where: { $0["message_id"]  as? String ?? "" == self.dataMessages[idx!]["message_id"]  as? String ?? "" })
+                            if row != nil && section != nil  {
+                                DispatchQueue.main.async {
+                                    self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
+                                }
+                            }
+                        }
+                        completion(true)
+                    } else {
+                        DispatchQueue.main.async {
+                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Failed to pin or unpin message, make sure you are connected to internet".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                            banner.show()
+                        }
+                        completion(false)
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                        banner.show()
+                    }
+                    completion(false)
+                }
+            }
+        }
+    }
+    
     func showEditMessageView(at indexPath: IndexPath) {
         let dataMessages = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[indexPath.section]})
         let oldText = dataMessages[indexPath.row][TypeDataMessage.message_text]  as? String ?? ""
@@ -6093,6 +6286,22 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
         cell.contentView.addSubview(containerMessage)
         containerMessage.translatesAutoresizingMaskIntoConstraints = false
         
+        if messageIdChat.contains("NTFPIN_") {
+            containerMessage.backgroundColor = .orangeColor
+            containerMessage.anchor(top: cell.contentView.topAnchor, bottom: cell.contentView.bottomAnchor, paddingTop: 5, paddingBottom: 5, centerX: cell.contentView.centerXAnchor, minWidth: 40, maxWidth: UIScreen.main.bounds.width - 40)
+            containerMessage.layer.cornerRadius = 15
+            containerMessage.clipsToBounds = true
+            
+            let textMessage = UILabel()
+            containerMessage.addSubview(textMessage)
+            textMessage.textAlignment = .center
+            textMessage.anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, right: containerMessage.rightAnchor, paddingTop: 10, paddingLeft: 10, paddingBottom: 10, paddingRight: 10)
+            textMessage.font = .systemFont(ofSize: 14)
+            textMessage.text = dataMessages[indexPath.row][TypeDataMessage.message_text]  as? String ?? ""
+            textMessage.textColor = .white
+            return cell
+        }
+        
         let timeMessage = UILabel()
         timeMessage.numberOfLines = 0
         cell.contentView.addSubview(timeMessage)
@@ -6302,6 +6511,31 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
         
         let imageAckView = UIImageView()
         let imageCredentialView = UIImageView()
+        let imagePinView = UIImageView()
+        if dataMessages[indexPath.row][TypeDataMessage.is_pinned] as? String != nil && dataMessages[indexPath.row][TypeDataMessage.is_pinned] as? String != "0" {
+            cell.contentView.addSubview(imagePinView)
+            imagePinView.translatesAutoresizingMaskIntoConstraints = false
+            if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+                if imageStared.isDescendant(of: cell.contentView){
+                    imagePinView.bottomAnchor.constraint(equalTo: imageStared.topAnchor).isActive = true
+                } else {
+                    imagePinView.bottomAnchor.constraint(equalTo: statusMessage.topAnchor).isActive = true
+                }
+                imagePinView.trailingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: -8).isActive = true
+            } else {
+                if imageStared.isDescendant(of: cell.contentView){
+                    imagePinView.bottomAnchor.constraint(equalTo: imageStared.topAnchor).isActive = true
+                } else {
+                    imagePinView.bottomAnchor.constraint(equalTo: timeMessage.topAnchor).isActive = true
+                }
+                imagePinView.leadingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: 8).isActive = true
+            }
+            imagePinView.widthAnchor.constraint(equalToConstant: 15).isActive = true
+            imagePinView.heightAnchor.constraint(equalToConstant: 15).isActive = true
+            imagePinView.image = UIImage(systemName: "pin.fill")
+            imagePinView.backgroundColor = .clear
+            imagePinView.tintColor = .lightGray
+        }
         if dataMessages[indexPath.row]["read_receipts"] as? String == "8" && (dataMessages[indexPath.row]["lock"] as? String) != "2" && (dataMessages[indexPath.row]["lock"] as? String) != "1" {
             var imageAck = UIImage(named: "ack_icon_gray", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal)
             if dataMessages[indexPath.row]["status"] as? String == "8" {
@@ -7494,6 +7728,7 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
         
         if (!reffChat.isEmpty && dataMessages[indexPath.row]["message_scope_id"]  as? String ?? "" != MessageScope.FORM) {
             let data = queryMessageReply(message_id: reffChat)
+            let chatGroup = Chat.getMessageFromId(message_id: reffChat)
             if data.count != 0 {
                 let containerReply = UIView()
                 containerMessage.addSubview(containerReply)
@@ -7537,30 +7772,37 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 titleReply.topAnchor.constraint(equalTo: containerReply.topAnchor, constant: 10).isActive = true
                 titleReply.trailingAnchor.constraint(lessThanOrEqualTo: containerReply.trailingAnchor, constant: -20).isActive = true
                 titleReply.font = UIFont.systemFont(ofSize: 12 + offset()).bold
-                if (data["f_pin"] as? String == idMe) {
-                    titleReply.text = "You".localized()
-                    if dataMessages[indexPath.row]["f_pin"] as? String == idMe {
-                        titleReply.textColor = .white
-                        leftReply.backgroundColor = .white
+                let f_pin = chatGroup.count == 0 ? data["f_pin"] as? String : chatGroup[0].fpin
+                if f_pin == idMe {
+                    if chatGroup.count == 0 {
+                        titleReply.text = "You".localized()
                     } else {
-                        titleReply.textColor = .mainColor
-                        leftReply.backgroundColor = .mainColor
+                        if let dataPerson = User.getData(pin: f_pin) {
+                            titleReply.text = "You".localized() + " ● " + "\(chatGroup[0].groupName)(\(chatGroup[0].name))"
+                        }
                     }
                 } else {
                     if isContactCenter {
                         let user: [User] = users.filter({$0.pin == data["f_pin"] as? String})
                         titleReply.text = user.first!.fullName
                     } else {
-                        titleReply.text = self.dataPerson["name"]!!
-                    }
-                    if dataMessages[indexPath.row]["f_pin"] as? String == idMe {
-                        titleReply.textColor = .white
-                        leftReply.backgroundColor = .white
-                    } else {
-                        titleReply.textColor = .mainColor
-                        leftReply.backgroundColor = .mainColor
+                        if chatGroup.count == 0 {
+                            titleReply.text = self.dataPerson["name"]!!
+                        } else {
+                            if let dataPerson = User.getData(pin: f_pin) {
+                                let namePerson = dataPerson.fullName
+                                titleReply.text = namePerson + " ● " + "\(chatGroup[0].groupName)(\(chatGroup[0].name))"
+                            }
+                        }
                     }
                 }
+                if dataMessages[indexPath.row]["f_pin"] as? String == idMe {
+                    titleReply.textColor = .white
+                    leftReply.backgroundColor = .white
+                } else {
+                    titleReply.textColor = .mainColor
+                    leftReply.backgroundColor = .mainColor
+                }
                 
                 let contentReply = UILabel()
                 contentReply.numberOfLines = 2
@@ -7572,12 +7814,12 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 topConstraintContent.priority = .defaultHigh
                 topConstraintContent.isActive = true
                 contentReply.font = UIFont.systemFont(ofSize: 10 + offset())
-                let message_text = data["message_text"]  as? String ?? ""
-                let attachment_flag = data["attachment_flag"]  as? String ?? ""
-                let thumb_chat = data["thumb_id"]  as? String ?? ""
-                let image_chat = data["image_id"]  as? String ?? ""
-                let video_chat = data["video_id"]  as? String ?? ""
-                let file_chat = data["file_id"]  as? String ?? ""
+                let message_text = chatGroup.count == 0 ? (data["message_text"] as? String ?? "") : chatGroup[0].messageText
+                let attachment_flag = chatGroup.count == 0 ? (data["attachment_flag"] as? String ?? "") : chatGroup[0].attachmentFlag
+                let thumb_chat = chatGroup.count == 0 ? (data["thumb_id"] as? String ?? "") : chatGroup[0].thumb
+                let image_chat = chatGroup.count == 0 ? (data["image_id"] as? String ?? "") : chatGroup[0].image
+                let video_chat = chatGroup.count == 0 ? (data["video_id"] as? String ?? "") : chatGroup[0].video
+                let file_chat = chatGroup.count == 0 ? (data["file_id"] as? String ?? "") : chatGroup[0].file
                 if (attachment_flag == "0" && thumb_chat == "") {
                     contentReply.trailingAnchor.constraint(equalTo: containerReply.trailingAnchor, constant: -20).isActive = true
                     contentReply.attributedText = message_text.richText()
@@ -8337,6 +8579,16 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 }
             }
         } else {
+            let chatGroup = Chat.getMessageFromId(message_id: sender.message_id)
+            if chatGroup.count > 0 && chatGroup[0].messageScope == "4" {
+                let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
+                editorGroupVC.hidesBottomBarWhenPushed = true
+                editorGroupVC.unique_l_pin = chatGroup[0].pin == chatGroup[0].groupId ? chatGroup[0].groupId : chatGroup[0].pin
+                editorGroupVC.referenceMessageId = sender.message_id
+                editorGroupVC.referenceChatDate = chatDate(stringDate: chatGroup[0].serverDate)
+                navigationController?.show(editorGroupVC, sender: nil)
+                return
+            }
             DispatchQueue.main.async {
                 let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == sender.message_id})
                 if idx == nil {
@@ -8445,8 +8697,177 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
 //        return config
 //    }
     
+    private func pinAllMessages(dataMessages: [[String: Any?]], isPinned: Int = -1) {
+        var dataMessages = dataMessages
+        dataMessages.sort {
+            let firstPinned = Int64($0[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+            let secondPinned = Int64($1[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+            return firstPinned < secondPinned
+        }
+        if dataMessages.count != 0 {
+            if !self.containerPin.isDescendant(of: self.view) && dataMessages.count != 0 {
+                self.tableChatView.contentInset.top = 50
+                
+                self.view.addSubview(self.containerPin)
+                self.containerPin.isUserInteractionEnabled = true
+                let tapGesture = UITapGestureRecognizer(target: self, action: #selector(viewPinTapped))
+                self.containerPin.addGestureRecognizer(tapGesture)
+                self.containerPin.anchor(top: self.view.safeAreaLayoutGuide.topAnchor, left: self.view.leftAnchor, right: self.view.rightAnchor, height: 50)
+                self.containerPin.backgroundColor = .mainColor
+                
+                if dataMessages.count > 1 {
+                    self.containerPin.addSubview(self.signSelectedPin)
+                    self.signSelectedPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 8, centerY: self.containerPin.centerYAnchor, width: 2, height: 30)
+                    self.signSelectedPin.layer.cornerRadius = 1
+                    self.signSelectedPin.clipsToBounds = true
+                    self.signSelectedPin.alignment = .fill
+                    self.signSelectedPin.axis = .vertical
+                    self.signSelectedPin.distribution = .fill
+                    self.signSelectedPin.spacing = dataMessages.count == 3 ? 1.5 : 2
+                    
+                    let heightSign: CGFloat = CGFloat((30 / dataMessages.count) - 1)
+                    let widthSign: CGFloat = 2
+
+                    for i in 0..<dataMessages.count {
+                        let viewSign = UIView()
+                        viewSign.backgroundColor = (i == (dataMessages.count - 1)) ? .white : .gray
+                        viewSign.anchor(width: widthSign, height: heightSign)
+                        viewSign.layer.cornerRadius = 1
+                        viewSign.clipsToBounds = true
+                        self.signSelectedPin.addArrangedSubview(viewSign)
+                    }
+                    self.nextPinShowed = dataMessages.count - 1
+                }
+                
+                let contIconPin = UIImageView()
+                self.containerPin.addSubview(contIconPin)
+                contIconPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 15, centerY: self.containerPin.centerYAnchor, width: 30, height: 30)
+                contIconPin.layer.cornerRadius = 8
+                contIconPin.clipsToBounds = true
+                contIconPin.backgroundColor = .gray
+                contIconPin.image = UIImage(systemName: "pin.fill")?.imageWithInsets(insets: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5))?.withTintColor(.waGrayLight)
+                
+                self.containerPin.addSubview(textPin)
+                self.textPin.anchor(left: contIconPin.rightAnchor, right: self.containerPin.rightAnchor, paddingLeft: 10, paddingRight: 10, centerY: self.containerPin.centerYAnchor)
+                self.textPin.attributedText = (dataMessages[dataMessages.count - 1][TypeDataMessage.message_text] as? String ?? "").richText(fontSize: 14)
+                self.textPin.numberOfLines = 1
+                self.textPin.textColor = .white
+            } else {
+                self.signSelectedPin.subviews.forEach({ $0.removeFromSuperview() })
+                self.signSelectedPin.removeFromSuperview()
+                var same = false
+                if dataMessages.count > 1 {
+                    self.containerPin.addSubview(self.signSelectedPin)
+                    self.signSelectedPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 8, centerY: self.containerPin.centerYAnchor, width: 2, height: 30)
+                    self.signSelectedPin.layer.cornerRadius = 1
+                    self.signSelectedPin.clipsToBounds = true
+                    self.signSelectedPin.alignment = .fill
+                    self.signSelectedPin.axis = .vertical
+                    self.signSelectedPin.distribution = .fill
+                    self.signSelectedPin.spacing = dataMessages.count == 3 ? 1.5 : 2
+                    
+                    let heightSign: CGFloat = CGFloat((30 / dataMessages.count) - 1)
+                    let widthSign: CGFloat = 2
+                    
+                    for i in 0..<dataMessages.count {
+                        let viewSign = UIView()
+                        viewSign.backgroundColor = (i == (dataMessages.count - 1)) ? .white : .gray
+                        viewSign.anchor(width: widthSign, height: heightSign)
+                        viewSign.layer.cornerRadius = 1
+                        viewSign.clipsToBounds = true
+                        self.signSelectedPin.addArrangedSubview(viewSign)
+                    }
+                    if isPinned == -1 {
+                        self.nextPinShowed = dataMessages.count - 1
+                    } else if self.nextPinShowed != 0 {
+                        if (self.nextPinShowed > isPinned) {
+                            self.nextPinShowed-=1
+                            same = true
+                        } else if self.nextPinShowed == isPinned && dataMessages.count == 3 {
+                            self.nextPinShowed-=2
+                        }
+                    } else if self.nextPinShowed != isPinned {
+                        same = true
+                    }
+                } else if self.nextPinShowed != isPinned {
+                    same = true
+                }
+                if !same{
+                    animateLabelTextChange(label: self.textPin, newText: dataMessages[dataMessages.count - 1][TypeDataMessage.message_text] as? String ?? "")
+                }
+            }
+        } else if self.containerPin.isDescendant(of: self.view) {
+            self.containerPin.subviews.forEach({ $0.removeFromSuperview() })
+            self.containerPin.removeFromSuperview()
+            self.tableChatView.contentInset.top = 0
+        }
+    }
+    
+    @objc func viewPinTapped() {
+        var dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
+        dataMessagesPin.sort {
+            let firstPinned = Int64($0[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+            let secondPinned = Int64($1[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
+            return firstPinned < secondPinned
+        }
+        let obj = ObjectGesture()
+        obj.message_id = dataMessagesPin[nextPinShowed][TypeDataMessage.message_id] as? String ?? ""
+        contentMessageTapped(obj)
+        
+        if nextPinShowed < dataMessagesPin.count - 1 {
+            nextPinShowed+=1
+        } else {
+            nextPinShowed = 0
+        }
+        
+        DispatchQueue.main.async {
+            self.signSelectedPin.subviews.forEach({ $0.removeFromSuperview() })
+            self.signSelectedPin.removeFromSuperview()
+            self.containerPin.addSubview(self.signSelectedPin)
+            self.signSelectedPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 8, centerY: self.containerPin.centerYAnchor, width: 2, height: 30)
+            self.signSelectedPin.layer.cornerRadius = 1
+            self.signSelectedPin.clipsToBounds = true
+            self.signSelectedPin.alignment = .fill
+            self.signSelectedPin.axis = .vertical
+            self.signSelectedPin.distribution = .fill
+            self.signSelectedPin.spacing = dataMessagesPin.count == 3 ? 1.5 : 2
+            
+            let heightSign: CGFloat = CGFloat((30 / dataMessagesPin.count) - 1)
+            let widthSign: CGFloat = 2
+
+            for i in 0..<dataMessagesPin.count {
+                let viewSign = UIView()
+                viewSign.backgroundColor = (i == self.nextPinShowed) ? .white : .gray
+                viewSign.anchor(width: widthSign, height: heightSign)
+                viewSign.layer.cornerRadius = 1
+                viewSign.clipsToBounds = true
+                self.signSelectedPin.addArrangedSubview(viewSign)
+            }
+            self.animateLabelTextChange(label: self.textPin, newText: dataMessagesPin[self.nextPinShowed][TypeDataMessage.message_text] as? String ?? "")
+        }
+    }
+    
+    func animateLabelTextChange(label: UILabel, newText: String) {
+        let animationDuration = 0.1
+        UIView.animate(withDuration: animationDuration, animations: {
+            label.transform = CGAffineTransform(translationX: 0, y: -10)
+            label.alpha = 0
+        }) { _ in
+            // Change text after fade out
+            label.attributedText = newText.richText(fontSize: 14)
+            label.transform = CGAffineTransform(translationX: 0, y: 10)
+            
+            // Animate back to original position and fade in
+            UIView.animate(withDuration: animationDuration) {
+                label.transform = .identity
+                label.alpha = 1
+            }
+        }
+    }
+    
     private func handleReply(indexPath: IndexPath, dataMessagesImage: [String: Any?] = [:], reffId: String = "") {
         var dataMessages = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[indexPath.section]})
+        var chatGroup: [Chat] = []
         if reffId.isEmpty {
             self.deleteReplyView()
             if dataMessagesImage.count != 0 {
@@ -8457,9 +8878,12 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
             self.reffId = dataMessages[indexPath.row]["message_id"] as? String
         } else {
             dataMessages = self.dataMessages.filter({ $0["message_id"]  as? String ?? "" == reffId })
+            if dataMessages.count == 0  {
+                chatGroup = Chat.getMessageFromId(message_id: reffId)
+            }
             self.reffId = reffId
         }
-        if dataMessages.count == 0  {
+        if dataMessages.count == 0 && chatGroup.count == 0 {
             self.deleteReplyView()
             return
         }
@@ -8503,14 +8927,22 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
         titleReply.topAnchor.constraint(equalTo: self.containerPreviewReply.topAnchor, constant: 10).isActive = true
         titleReply.font = UIFont.systemFont(ofSize: 12 + offset()).bold
         let idMe = User.getMyPin() as String?
-        if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+        let f_pin = chatGroup.count == 0 ? (dataMessages[indexPath.row]["f_pin"] as? String ?? "") : chatGroup[0].fpin
+        if f_pin == idMe {
             titleReply.text = "You".localized()
         } else {
             if self.isContactCenter {
                 let user: [User] = self.users.filter({$0.pin == dataMessages[indexPath.row]["f_pin"] as? String})
                 titleReply.text = user.first!.fullName
             } else {
-                titleReply.text = self.dataPerson["name"]!!
+                if chatGroup.count == 0 {
+                    titleReply.text = self.dataPerson["name"]!!
+                } else {
+                    if let dataPerson = User.getData(pin: f_pin) {
+                        let namePerson = dataPerson.fullName
+                        titleReply.text = namePerson + " ● " + "\(chatGroup[0].groupName)(\(chatGroup[0].name))"
+                    }
+                }
             }
         }
         titleReply.textColor = .orangeColor
@@ -8522,12 +8954,12 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
         contentReply.trailingAnchor.constraint(equalTo: containerPreviewReply.trailingAnchor, constant: -20).isActive = true
         contentReply.topAnchor.constraint(equalTo: titleReply.bottomAnchor).isActive = true
         contentReply.font = UIFont.systemFont(ofSize: 10 + offset())
-        let message_text = dataMessages[indexPath.row]["message_text"]  as? String ?? ""
-        let attachment_flag = dataMessages[indexPath.row]["attachment_flag"]  as? String ?? ""
-        let thumb_chat = dataMessages[indexPath.row]["thumb_id"]  as? String ?? ""
-        let image_chat = dataMessages[indexPath.row]["image_id"]  as? String ?? ""
-        let video_chat = dataMessages[indexPath.row]["video_id"]  as? String ?? ""
-        let file_chat = dataMessages[indexPath.row]["file_id"]  as? String ?? ""
+        let message_text = chatGroup.count == 0 ? (dataMessages[indexPath.row]["message_text"] as? String ?? "") : chatGroup[0].messageText
+        let attachment_flag = chatGroup.count == 0 ? (dataMessages[indexPath.row]["attachment_flag"] as? String ?? "") : chatGroup[0].attachmentFlag
+        let thumb_chat = chatGroup.count == 0 ? (dataMessages[indexPath.row]["thumb_id"] as? String ?? "") : chatGroup[0].thumb
+        let image_chat = chatGroup.count == 0 ? (dataMessages[indexPath.row]["image_id"] as? String ?? "") : chatGroup[0].image
+        let video_chat = chatGroup.count == 0 ? (dataMessages[indexPath.row]["video_id"] as? String ?? "") : chatGroup[0].video
+        let file_chat = chatGroup.count == 0 ? (dataMessages[indexPath.row]["file_id"] as? String ?? "") : chatGroup[0].file
         if (attachment_flag == "0" && thumb_chat == "") {
             contentReply.attributedText = message_text.richText()
         } else if (attachment_flag == "1" || image_chat != "") {
@@ -8610,6 +9042,9 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
             imageSticker.widthAnchor.constraint(equalToConstant: 30).isActive = true
             imageSticker.heightAnchor.constraint(equalToConstant: 30).isActive = true
         }
+        if chatGroup.count > 0 {
+            self.textFieldSend.becomeFirstResponder()
+        }
     }
     
     func scrollToFirstSearchMessage(indexScroll: Int = 1) {
@@ -8859,6 +9294,7 @@ public class TypeDataMessage {
     public static let last_edit = "last_edit"
     public static let gif_id = "gif_id"
     public static let is_forwarded = "is_forwarded"
+    public static let is_pinned = "is_pinned"
     public static let is_secret = "is_secret"
     public static let spec_file = "spec_file"
 }

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

@@ -442,7 +442,7 @@ class ContactChatViewController: UITableViewController {
             let allChats = Chat.getData()
             self.archivedChats = Chat.getData(isArchived: true)
             var tempChats: [Chat] = []
-            var lowestPinned: [String: Int] = [:]
+            var lowestPinned: [String: Int64] = [:]
 
             for singleChat in allChats {
                 guard !singleChat.groupId.isEmpty else {
@@ -1159,6 +1159,9 @@ extension ContactChatViewController {
     }
     
     override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+        if segment.numberOfSegments != 3 || (segment.numberOfSegments == 3 && segment.selectedSegmentIndex != 0) {
+            return nil
+        }
         let data: Chat
         if isFilltering {
             data = fillteredData[indexPath.row] as! Chat
@@ -1583,7 +1586,7 @@ extension ContactChatViewController {
                                     stringMessage.append(NSAttributedString(string: "You".localized() + ": ", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12 + String.offset(), weight: .medium)]))
                                 }
                                 stringMessage.append(("🚫 _"+"You were deleted this message".localized()+"_").richText())
-                            } else if data.messageScope != MessageScope.CALL && data.messageScope != MessageScope.MISSED_CALL {
+                            } else if data.messageScope != MessageScope.CALL && data.messageScope != MessageScope.MISSED_CALL && !data.messageId.contains("NTFPIN_") {
                                 let imageStatus = NSTextAttachment()
                                 let status = getRealStatus(messageId: data.messageId)
                                 if status == "0" {