alqindiirsyam 2 ani în urmă
părinte
comite
7d8b7a8670

+ 3 - 2
appbuilder-ios/AppBuilder/AppBuilder/SecondTabViewController.swift

@@ -1046,11 +1046,9 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
             let group: Group
             if isFilltering {
                 if indexPath.row == 0 {
-                    print("DATA GROUP1 \((fillteredData[indexPath.section] as! Group).name)")
                     group = fillteredData[indexPath.section] as! Group
                 } else {
                     if (fillteredData[indexPath.section] as! Group).childs.count > 0 {
-                        print("DATA GROUP2 \((fillteredData[indexPath.section] as! Group).name)")
                         group = (fillteredData[indexPath.section] as! Group).childs[indexPath.row - 1]
                     } else {
                         return cell
@@ -1058,6 +1056,9 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                 }
             } else {
                 if indexPath.row == 0 {
+                    if indexPath.section > (groups.count - 1) {
+                        return cell
+                    }
                     group = groups[indexPath.section]
                 } else {
                     group = groups[indexPath.section].childs[indexPath.row - 1]

+ 22 - 12
appbuilder-ios/NexilisLite/NexilisLite/Source/Extension.swift

@@ -380,7 +380,7 @@ extension UIColor {
     }
     
     public static var linkColor: UIColor {
-        return renderColor(hex: "#68BBE3")
+        return renderColor(hex: "#0000EE")
     }
     
     public static var blueBubbleColor: UIColor {
@@ -628,7 +628,7 @@ extension String {
         return length
     }
     
-    public func richText(isEditing: Bool = false, isSearching: Bool = false, textSearch: String = "") -> NSAttributedString {
+    public func richText(isEditing: Bool = false, isSearching: Bool = false, textSearch: String = "", group_id: String = "", listMentionInTextField: [User] = []) -> NSMutableAttributedString {
         let font = UIFont.systemFont(ofSize: 12)
         let textUTF8 = String(self.utf8)
         let finalText = NSMutableAttributedString(string: textUTF8, attributes: [NSAttributedString.Key.font: font])
@@ -784,16 +784,26 @@ extension String {
             }
         }
         
-        if !isEditing {
-            let listTextEnter = finalText.string.split(separator: "\n")
-            for j in 0...listTextEnter.count - 1 {
-                let listText = listTextEnter[j].split(separator: " ")
-                for i in 0...listText.count - 1 {
-                    if listText[i].lowercased().checkStartWithLink() {
-                        if let range: Range<String.Index> = finalText.string.range(of: listText[i]) {
-                            let index: Int = finalText.string.distance(from: finalText.string.startIndex, to: range.lowerBound)
-                            finalText.addAttribute(.foregroundColor, value: UIColor.linkColor, range: NSRange(index...index + listText[i].count - 1))
-                            finalText.addAttribute(.underlineStyle, value: NSUnderlineStyle.thick.rawValue, range: NSRange(index...index + listText[i].count - 1))
+        //Check Mention
+        let finalTextAfterRichText = finalText.string
+        if finalTextAfterRichText.contains("@") {
+            let listMembers = Member.getAllMember(group_id: group_id)
+            if listMembers.count > 0 {
+                for i in 0..<listMembers.count {
+                    if isEditing {
+                        if listMentionInTextField.count > 0 {
+                            let name = (listMembers[i].firstName + " " + listMembers[i].lastName).trimmingCharacters(in: .whitespaces)
+                            if listMentionInTextField.firstIndex(where: { ($0.firstName + " " + $0.lastName).trimmingCharacters(in: .whitespaces) == name }) != nil {
+                                let range = NSString(string: finalTextAfterRichText).range(of: "@\(name)", options: .caseInsensitive)
+                                finalText.addAttribute(.foregroundColor, value: UIColor.orangeColor, range: range)
+                            }
+                        }
+                    } else {
+                        let name = (listMembers[i].firstName + " " + listMembers[i].lastName).trimmingCharacters(in: .whitespaces)
+                        let range = NSString(string: finalTextAfterRichText).range(of: listMembers[i].pin, options: .caseInsensitive)
+                        if range.lowerBound != range.upperBound {
+                            finalText.mutableString.replaceOccurrences(of: listMembers[i].pin, with: name, options: .literal, range: range)
+                            finalText.addAttribute(.foregroundColor, value: UIColor.orangeColor, range: NSString(string: finalText.string).range(of: "@\(name)"))
                         }
                     }
                 }

+ 16 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/Model/Group.swift

@@ -135,4 +135,20 @@ public class Member: User {
         super.init(pin: pin, firstName: firstName, lastName: lastName, thumb: thumb)
     }
     
+    public static func getAllMember(group_id: String) -> [Member] {
+        var members: [Member] = []
+        Database.shared.database?.inTransaction({ fmdb, rollback in
+            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name from GROUPZ_MEMBER where group_id = '\(group_id)' OR group_id = (select group_id from DISCUSSION_FORUM where chat_id = '\(group_id)')") {
+                while cursor.next() {
+                    members.append(Member(pin: cursor.string(forColumnIndex: 0) ?? "",
+                                          firstName: cursor.string(forColumnIndex: 1) ?? "",
+                                          lastName: cursor.string(forColumnIndex: 2) ?? "",
+                                          thumb: "",
+                                          position: ""))
+                }
+                cursor.close()
+            }
+        })
+        return members
+    }
 }

+ 23 - 7
appbuilder-ios/NexilisLite/NexilisLite/Source/Model/User.swift

@@ -10,9 +10,9 @@ import FMDB
 
 public class User: Model {
     
-    public let pin: String
-    public let firstName: String
-    public let lastName: String
+    public var pin: String
+    public var firstName: String
+    public var lastName: String
     public var thumb: String
     public var official: String?
     public var userType: String?
@@ -144,13 +144,13 @@ public class User: Model {
         return user
     }
     
-    public static func getDataCanNil(pin: String?) -> User? {
+    public static func getDataCanNil(pin: String?, fmdb: FMDatabase? = nil) -> User? {
         guard let pin = pin else {
             return nil
         }
         var user: User?
-        Database.shared.database?.inTransaction({ fmdb, rollback in
-            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name, image_id, user_type, privacy_flag, offline_mode, ex_block, device_id, official_account from BUDDY where f_pin = '\(pin)' OR device_id = '\(pin)'"), cursor.next() {
+        if fmdb != nil {
+            if let cursor = Database.shared.getRecords(fmdb: fmdb!, query: "select f_pin, first_name, last_name, image_id, user_type, privacy_flag, offline_mode, ex_block, device_id, official_account from BUDDY where f_pin = '\(pin)' OR device_id = '\(pin)'"), cursor.next() {
                 user = User(pin: cursor.string(forColumnIndex: 0) ?? "",
                             firstName: cursor.string(forColumnIndex: 1) ?? "",
                             lastName: cursor.string(forColumnIndex: 2) ?? "",
@@ -163,7 +163,23 @@ public class User: Model {
                             device_id: cursor.string(forColumnIndex: 8) ?? "")
                 cursor.close()
             }
-        })
+        } else {
+            Database.shared.database?.inTransaction({ fmdb, rollback in
+                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name, image_id, user_type, privacy_flag, offline_mode, ex_block, device_id, official_account from BUDDY where f_pin = '\(pin)' OR device_id = '\(pin)'"), cursor.next() {
+                    user = User(pin: cursor.string(forColumnIndex: 0) ?? "",
+                                firstName: cursor.string(forColumnIndex: 1) ?? "",
+                                lastName: cursor.string(forColumnIndex: 2) ?? "",
+                                thumb: cursor.string(forColumnIndex: 3) ?? "",
+                                userType: cursor.string(forColumnIndex: 4) ?? "",
+                                privacy_flag: cursor.string(forColumnIndex: 5) ?? "",
+                                offline_mode: cursor.string(forColumnIndex: 6) ?? "",
+                                ex_block: cursor.string(forColumnIndex: 7) ?? "",
+                                official: cursor.string(forColumnIndex: 9) ?? "",
+                                device_id: cursor.string(forColumnIndex: 8) ?? "")
+                    cursor.close()
+                }
+            })
+        }
         return user
     }
     

+ 9 - 6
appbuilder-ios/NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -1003,6 +1003,7 @@ public class Nexilis: NSObject {
             let broadcast_flag = message.getBody(key: CoreMessage_TMessageKey.BROADCAST_FLAG, default_value: "0")
             let is_call_center = message.getBody(key: CoreMessage_TMessageKey.IS_CALL_CENTER, default_value: "0")
             let call_center_id = message.getBody(key: CoreMessage_TMessageKey.CALL_CENTER_ID, default_value: "")
+            let opposite_pin = message.getBody(key: CoreMessage_TMessageKey.OPPOSITE_PIN, default_value: "")
             print("prepare save db")
             do {
                 _ = try Database.shared.insertRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
@@ -1097,17 +1098,19 @@ public class Nexilis: NSObject {
             }
             if is_call_center == "0" {
                 do {
-                    let queryGetLastMessageId = "SELECT message_id FROM MESSAGE where opposite_pin = '\(f_pin)' order by server_date desc LIMIT 1"
+                    let queryGetLastMessageId = "SELECT message_id FROM MESSAGE where opposite_pin = '\(opposite_pin)' OR l_pin = '\(pin)' order by server_date desc LIMIT 1"
                     var messageId = ""
                     if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: queryGetLastMessageId), cursorData.next() {
                         messageId = cursorData.string(forColumnIndex: 0) ?? ""
                         cursorData.close()
                     }
-                    _ = try Database.shared.insertRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
-                        "l_pin" : pin,
-                        "message_id" : messageId,
-                        "counter" : counter!
-                    ], replace: true)
+                    if !messageId.isEmpty {
+                        _ = try Database.shared.insertRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
+                            "l_pin" : pin,
+                            "message_id" : messageId,
+                            "counter" : counter!
+                        ], replace: true)
+                    }
                 } catch {
                     rollback.pointee = true
                     print(error)

+ 11 - 11
appbuilder-ios/NexilisLite/NexilisLite/Source/Utils.swift

@@ -90,35 +90,35 @@ public final class Utils {
     
     public static func previewMessageText(chat: Chat) -> Any {
         if chat.credential == "1" && chat.lock == "2" {
-            return ("🚫 _"+"Message has expired".localized()+"_").richText()
+            return ("🚫 _"+"Message has expired".localized()+"_").richText(group_id: chat.pin)
         } else if chat.credential == "1" {
-            return "Confidential Message".localized().richText()
+            return "Confidential Message".localized().richText(group_id: chat.pin)
         } else if chat.attachmentFlag == "27" {
-            return ("📄 " + "Live Streaming".localized()).richText()
+            return ("📄 " + "Live Streaming".localized()).richText(group_id: chat.pin)
         } else if !chat.image.isEmpty {
             if !chat.messageText.isEmpty {
-                return "📷 \(chat.messageText)".richText()
+                return "📷 \(chat.messageText)".richText(group_id: chat.pin)
             } else {
-                return ("📷 " + "Photo".localized()).richText()
+                return ("📷 " + "Photo".localized()).richText(group_id: chat.pin)
             }
         }
         else if !chat.video.isEmpty {
             if !chat.messageText.isEmpty {
-                return "📹 \(chat.messageText)".richText()
+                return "📹 \(chat.messageText)".richText(group_id: chat.pin)
             } else {
-                return ("📹 " + "Video".localized()).richText()
+                return ("📹 " + "Video".localized()).richText(group_id: chat.pin)
             }
         }
         else if !chat.file.isEmpty {
             if chat.messageScope == "18" {
-                return ("📄 Form").richText()
+                return ("📄 Form").richText(group_id: chat.pin)
             }
-            return ("📄 " + chat.messageText.components(separatedBy: "|")[0]).richText()
+            return ("📄 " + chat.messageText.components(separatedBy: "|")[0]).richText(group_id: chat.pin)
         } else if chat.attachmentFlag == "11" {
-            return ("❤️ " + "Sticker".localized()).richText()
+            return ("❤️ " + "Sticker".localized()).richText(group_id: chat.pin)
         }
         else {
-            return chat.messageText.richText()
+            return chat.messageText.richText(group_id: chat.pin)
         }
     }
     

+ 520 - 112
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -80,7 +80,15 @@ public class EditorGroup: UIViewController {
     var buttonDown: UIButton!
     var keyboardHeightForMention: CGFloat?
     var listMentionWithText:[User] = []
+    var listMentionInTextField:[User] = []
     var showingLink = ""
+    var isAlwaysHideLinkPreview = false
+    var timerCheckLink: Timer?
+    var lastPositionCursorMention = 0
+    var timerLongPressLink: Timer?
+    var lastTouchPoint: CGPoint = .zero
+    var isLinkCopied = false
+    var touchedSubview = UIView()
     
     public override func viewDidDisappear(_ animated: Bool) {
         if self.isMovingFromParent {
@@ -1211,7 +1219,7 @@ public class EditorGroup: UIViewController {
         if (reffId != nil) {
             reff_id = reffId!
         }
-        let message_text = message_text.trimmingCharacters(in: .whitespacesAndNewlines)
+        var message_text = message_text.trimmingCharacters(in: .whitespacesAndNewlines)
         let idMe = UserDefaults.standard.string(forKey: "me") as String?
         var opposite_pin = self.dataGroup["group_id"] as! String
         if (self.dataTopic["chat_id"] as! String != "") {
@@ -1221,6 +1229,19 @@ public class EditorGroup: UIViewController {
         if isAck {
             read_receipts = "8"
         }
+        if message_text.contains("@") && listMentionInTextField.count > 0 {
+            for i in 0..<listMentionInTextField.count {
+                let nameWithMention = ("@" + listMentionInTextField[i].firstName + " " + listMentionInTextField[i].lastName).trimmingCharacters(in: .whitespaces)
+                let upperBound = Int(listMentionInTextField[i].ex_block!)! - 1
+                let lowerBound = Int(listMentionInTextField[i].ex_block!)! - nameWithMention.count
+                var afterMention = ""
+                if upperBound + 1 != message_text.count && message_text.substring(from: upperBound + 1, to: upperBound + 1) != "\n" && message_text.substring(from: upperBound + 1, to: upperBound + 1) != " " && message_text.substring(from: upperBound + 1, to: upperBound + 1) != "" {
+                    afterMention = " "
+                }
+                let stringRange = message_text.index(message_text.startIndex, offsetBy: lowerBound)..<message_text.index(message_text.startIndex, offsetBy: upperBound + 1)
+                message_text = message_text.replacingOccurrences(of: nameWithMention, with: "@\(listMentionInTextField[i].pin)" + afterMention, range: stringRange)
+            }
+        }
         let message = CoreMessage_TMessageBank.sendMessage(l_pin: dataGroup["group_id"] as! String, message_scope_id: message_scope_id, status: status, message_text: message_text, credential: credential, attachment_flag: attachment_flag, ex_blog_id: ex_blog_id, message_large_text: message_large_text, ex_format: ex_format, image_id: image_id, audio_id: audio_id, video_id: video_id, file_id: file_id, thumb_id: thumb_id, reff_id: reff_id, read_receipts: read_receipts, chat_id: dataTopic["chat_id"] as! String, is_call_center: is_call_center, call_center_id: call_center_id, opposite_pin: opposite_pin)
         Nexilis.addQueueMessage(message: message)
         let messageId = String(message.mBodies[CoreMessage_TMessageKey.MESSAGE_ID]!)
@@ -1261,6 +1282,7 @@ public class EditorGroup: UIViewController {
         }
         deleteReplyView()
         deleteLinkPreview()
+        listMentionInTextField.removeAll()
         DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
             self.tableChatView.scrollToBottom()
             if self.markerCounter != nil {
@@ -1598,17 +1620,51 @@ extension EditorGroup: UIDocumentPickerDelegate, DocumentPickerDelegate, QLPrevi
 
 extension EditorGroup: UITextViewDelegate {
     public func textViewDidChangeSelection(_ textView: UITextView) {
-        let cursorPositionInt = textView.selectedRange.location
+        lastPositionCursorMention = textView.selectedRange.location
+        let fulltextForMention = textView.text.substring(from: 0, to: lastPositionCursorMention - 1)
         var isShowMention = false
-        if cursorPositionInt > 0 {
-            if textView.text.contains("@") {
-                let fulltext = textView.text.substring(from: 0, to: cursorPositionInt - 1)
-                let splitBreak = fulltext.components(separatedBy: "\n")
+        if lastPositionCursorMention > 0 {
+            var listHaveToRemoved: [User] = []
+            var continueCheckMention = true
+            if listMentionInTextField.count > 0 {
+                for i in 0..<listMentionInTextField.count {
+                    if listMentionInTextField[i].ex_block != nil && !listMentionInTextField[i].ex_block!.isEmpty {
+                        let nameWithMention = ("@" + listMentionInTextField[i].firstName + " " + listMentionInTextField[i].lastName).trimmingCharacters(in: .whitespaces)
+                        var rangeLower = Int(listMentionInTextField[i].ex_block!)! - nameWithMention.count
+                        var rangeUpper = Int(listMentionInTextField[i].ex_block!)!
+                        if textView.text.substring(from: rangeLower, to: rangeUpper - 1) == nameWithMention {
+                            if lastPositionCursorMention >= rangeLower + 1 && lastPositionCursorMention <= rangeUpper {
+                                continueCheckMention = false
+                            }
+                        } else {
+                            if listMentionInTextField[i].ex_offmp!.isEmpty {
+                                rangeLower = rangeLower + listMentionWithText.count
+                                rangeUpper = rangeUpper + listMentionWithText.count
+                            } else {
+                                rangeLower = rangeLower + (listMentionWithText.count - Int(listMentionInTextField[i].ex_offmp!)!)
+                                rangeUpper = rangeUpper + (listMentionWithText.count - Int(listMentionInTextField[i].ex_offmp!)!)
+                            }
+                            if textView.text.substring(from: rangeLower, to: rangeUpper - 1) == nameWithMention {
+                                if lastPositionCursorMention >= rangeLower + 1 && lastPositionCursorMention <= rangeUpper {
+                                    continueCheckMention = false
+                                }
+                                listMentionInTextField[i].ex_block! = "\(rangeUpper)"
+                                listMentionInTextField[i].ex_offmp! = "\(textView.text.count)"
+                            } else {
+                                listHaveToRemoved.append(listMentionInTextField[i])
+                            }
+                        }
+                    }
+                }
+            }
+            listMentionInTextField.removeAll(where: { listHaveToRemoved.contains($0) })
+            if continueCheckMention {
+                let splitBreak = fulltextForMention.components(separatedBy: "\n")
                 let indexLastBreak = splitBreak.lastIndex(where: { $0.contains("@") })
                 if indexLastBreak != nil {
                     let splitSpace = splitBreak[indexLastBreak!].components(separatedBy: " ")
                     let indexLastMention = splitSpace.lastIndex(where: { $0.substring(from: 0, to: 0) == "@" })
-                    if indexLastMention != nil {
+                    if indexLastMention != nil && fulltextForMention.substring(from: lastPositionCursorMention - 1, to: lastPositionCursorMention - 1) != " " && fulltextForMention.substring(from: lastPositionCursorMention - 1, to: lastPositionCursorMention - 1) != "\n" {
                         let fullTextMention = splitSpace[indexLastMention!]
                         showMention(text: fullTextMention.substring(from: 1, to: fullTextMention.count))
                         isShowMention = true
@@ -1636,6 +1692,9 @@ extension EditorGroup: UITextViewDelegate {
     }
     
     public func textViewDidChange(_ textView: UITextView) {
+        if textView.text.count == 0 {
+            isAlwaysHideLinkPreview = false
+        }
         if allowTyping {
             allowTyping = false
             if dataTopic["chat_id"] as! String == "" {
@@ -1649,10 +1708,23 @@ extension EditorGroup: UITextViewDelegate {
                 self.allowTyping = true
             })
         }
-        checkLink(fullText: textView.text)
-        if textView.text.contains("*") || textView.text.contains("_") || textView.text.contains("^") || textView.text.contains("~") {
+        timerCheckLink?.invalidate()
+        timerCheckLink = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: {_ in
+            self.checkLink(fullText: textView.text)
+        })
+        
+        if listMentionInTextField.count > 0 {
+            for j in 0..<listMentionInTextField.count {
+                let name = (listMentionInTextField[j].firstName + " " + listMentionInTextField[j].lastName).trimmingCharacters(in: .whitespaces)
+                if !textView.text.contains("@\(name)") {
+                    listMentionInTextField.remove(at: j)
+                }
+            }
+        }
+        
+        if textView.text.contains("*") || textView.text.contains("_") || textView.text.contains("^") || textView.text.contains("~") || textView.text.contains("@") {
             textView.preserveCursorPosition(withChanges: { _ in
-                textView.attributedText = textView.text.richText(isEditing: true)
+                textView.attributedText = textView.text.richText(isEditing: true, group_id: self.dataGroup["group_id"] as! String, listMentionInTextField: listMentionInTextField)
                 return .preserveCursor
             })
         }
@@ -1661,6 +1733,12 @@ extension EditorGroup: UITextViewDelegate {
     private func showMention(text: String) {
         if self.contraintBottomMention.constant < 0 {
             self.contraintBottomMention.constant = 65 + ((self.keyboardHeightForMention != nil) ? self.keyboardHeightForMention! : 0)
+            if self.viewTextfield.subviews.contains(self.containerLink) {
+                self.contraintBottomMention.constant = self.contraintBottomMention.constant + 80
+            }
+            if self.viewTextfield.subviews.contains(self.containerPreviewReply) {
+                self.contraintBottomMention.constant = self.contraintBottomMention.constant + 50
+            }
             UIView.animate(withDuration: 0.5, animations: {
                 self.view.layoutIfNeeded()
             })
@@ -1668,15 +1746,23 @@ extension EditorGroup: UITextViewDelegate {
         listMentionWithText.removeAll()
         Database.shared.database?.inTransaction({ fmdb, rollback in
             let idMe = UserDefaults.standard.string(forKey: "me")!
-            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "SELECT thumb_id, f_pin, first_name || ' ' || ifnull(last_name, '') name FROM GROUPZ_MEMBER where group_id='\(self.dataGroup["group_id"] as! String)' AND f_pin <> '\(idMe)' AND name LIKE '%\(text)%'") {
+            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, first_name || ' ' || ifnull(last_name, '') name FROM GROUPZ_MEMBER where group_id='\(self.dataGroup["group_id"] as! String)' AND f_pin <> '\(idMe)' AND name LIKE '%\(text)%'") {
                 while cursor.next() {
-                    listMentionWithText.append(User(pin: cursor.string(forColumnIndex: 1) ?? "",
-                                                    firstName: cursor.string(forColumnIndex: 2) ?? "",
-                                                    lastName: cursor.string(forColumnIndex: 2) ?? "",
-                                                    thumb: cursor.string(forColumnIndex: 0) ?? ""))
+                    let user = User(pin: "")
+                    user.pin = cursor.string(forColumnIndex: 0) ?? ""
+                    user.firstName = cursor.string(forColumnIndex: 1) ?? ""
+                    if !user.pin.isEmpty {
+                        let userFromBuddy = User.getDataCanNil(pin: user.pin, fmdb: fmdb)
+                        if userFromBuddy != nil {
+                            listMentionWithText.append(userFromBuddy!)
+                        } else {
+                            listMentionWithText.append(user)
+                        }
+                    }
                 }
                 cursor.close()
             }
+            listMentionWithText.removeAll(where: { listMentionInTextField.contains($0) })
             if listMentionWithText.count > 0 {
                 if listMentionWithText.count < 5 {
                     self.heightTableMention.constant = CGFloat(44 * listMentionWithText.count)
@@ -1701,84 +1787,86 @@ extension EditorGroup: UITextViewDelegate {
     }
     
     private func checkLink(fullText: String) {
-        var text = ""
-        let listTextSplitBreak = fullText.components(separatedBy: "\n")
-        let indexFirstLinkSplitBreak = listTextSplitBreak.firstIndex(where: { $0.contains("www.") || $0.contains("http://") || $0.contains("https://") })
-        if indexFirstLinkSplitBreak != nil {
-            let listTextSplitSpace = listTextSplitBreak[indexFirstLinkSplitBreak!].components(separatedBy: " ")
-            let indexFirstLinkSplitSpace = listTextSplitSpace.firstIndex(where: { ($0.starts(with: "www.") && $0.components(separatedBy: ".").count > 2) || ($0.starts(with: "http://") && $0.components(separatedBy: ".").count > 1) || ($0.starts(with: "https://") && $0.components(separatedBy: ".").count > 1) })
-            if indexFirstLinkSplitSpace != nil {
-                text = listTextSplitSpace[indexFirstLinkSplitSpace!]
-            }
-        }
-        if !text.isEmpty {
-            var stringURl = text
-            if stringURl.starts(with: "www.") {
-                stringURl = "https://" + stringURl.replacingOccurrences(of: "www.", with: "")
-            }
-            var dataURL = ""
-            Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select data_link from LINK_PREVIEW where link='\(text)'") {
-                    while cursor.next() {
-                        if let data = cursor.string(forColumnIndex: 0) {
-                            dataURL = data
-                        }
-                    }
-                    cursor.close()
+        if !isAlwaysHideLinkPreview {
+            var text = ""
+            let listTextSplitBreak = fullText.components(separatedBy: "\n")
+            let indexFirstLinkSplitBreak = listTextSplitBreak.firstIndex(where: { $0.contains("www.") || $0.contains("http://") || $0.contains("https://") })
+            if indexFirstLinkSplitBreak != nil {
+                let listTextSplitSpace = listTextSplitBreak[indexFirstLinkSplitBreak!].components(separatedBy: " ")
+                let indexFirstLinkSplitSpace = listTextSplitSpace.firstIndex(where: { ($0.starts(with: "www.") && $0.components(separatedBy: ".").count > 2) || ($0.starts(with: "http://") && $0.components(separatedBy: ".").count > 1) || ($0.starts(with: "https://") && $0.components(separatedBy: ".").count > 1) })
+                if indexFirstLinkSplitSpace != nil {
+                    text = listTextSplitSpace[indexFirstLinkSplitSpace!]
                 }
-            })
-            if !dataURL.isEmpty {
-                if let data = try! JSONSerialization.jsonObject(with: dataURL.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
-                    let title = data["title"] as! String
-                    let description = data["description"] as! String
-                    let imageUrl = data["imageUrl"] as? String
-                    let link = data["link"] as! String
-                    if self.showingLink != text {
-                        self.showingLink = text
-                        self.deleteLinkPreview()
-                        self.buildPreviewLink(imageUrl: imageUrl, title: title, description: description, stringURl: link)
-                    }
+            }
+            if !text.isEmpty {
+                var stringURl = text
+                if stringURl.starts(with: "www.") {
+                    stringURl = "https://" + stringURl.replacingOccurrences(of: "www.", with: "")
                 }
-            } else {
-                let urlData = URL(string: stringURl)!
-                Readability.parse(url: urlData, completion: { data in
-                    if data != nil {
-                        let title = data!.title
-                        let description = data!.description
-                        let imageUrl = data!.topImage
-                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                            do {
-                                var dataJson: [String: Any] = [:]
-                                dataJson["title"] = title
-                                dataJson["description"] = description
-                                dataJson["imageUrl"] = imageUrl
-                                dataJson["link"] = text
-                                guard let json = String(data: try! JSONSerialization.data(withJSONObject: dataJson, options: []), encoding: String.Encoding.utf8) else {
-                                    return
-                                }
-                                _ = try Database.shared.insertRecord(fmdb: fmdb, table: "LINK_PREVIEW", cvalues: [
-                                    "id" : "\(Date().currentTimeMillis().toHex())",
-                                    "link" : text,
-                                    "data_link" : json,
-                                    "retry": 0
-                                ], replace: true)
-                            } catch {
-                                rollback.pointee = true
-                                print(error)
+                var dataURL = ""
+                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                    if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select data_link from LINK_PREVIEW where link='\(text)'") {
+                        while cursor.next() {
+                            if let data = cursor.string(forColumnIndex: 0) {
+                                dataURL = data
                             }
-                        })
+                        }
+                        cursor.close()
+                    }
+                })
+                if !dataURL.isEmpty {
+                    if let data = try! JSONSerialization.jsonObject(with: dataURL.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
+                        let title = data["title"] as! String
+                        let description = data["description"] as! String
+                        let imageUrl = data["imageUrl"] as? String
+                        let link = data["link"] as! String
                         if self.showingLink != text {
                             self.showingLink = text
                             self.deleteLinkPreview()
-                            self.buildPreviewLink(imageUrl: imageUrl, title: title, description: description, stringURl: text)
-                        } else {
-                            self.deleteLinkPreview()
+                            self.buildPreviewLink(imageUrl: imageUrl, title: title, description: description, stringURl: link)
                         }
                     }
-                })
+                } else {
+                    let urlData = URL(string: stringURl)!
+                    Readability.parse(url: urlData, completion: { data in
+                        if data != nil {
+                            let title = data!.title
+                            let description = data!.description
+                            let imageUrl = data!.topImage
+                            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                do {
+                                    var dataJson: [String: Any] = [:]
+                                    dataJson["title"] = title
+                                    dataJson["description"] = description
+                                    dataJson["imageUrl"] = imageUrl
+                                    dataJson["link"] = text
+                                    guard let json = String(data: try! JSONSerialization.data(withJSONObject: dataJson, options: []), encoding: String.Encoding.utf8) else {
+                                        return
+                                    }
+                                    _ = try Database.shared.insertRecord(fmdb: fmdb, table: "LINK_PREVIEW", cvalues: [
+                                        "id" : "\(Date().currentTimeMillis().toHex())",
+                                        "link" : text,
+                                        "data_link" : json,
+                                        "retry": 0
+                                    ], replace: true)
+                                } catch {
+                                    rollback.pointee = true
+                                    print(error)
+                                }
+                            })
+                            if self.showingLink != text {
+                                self.showingLink = text
+                                self.deleteLinkPreview()
+                                self.buildPreviewLink(imageUrl: imageUrl, title: title, description: description, stringURl: text)
+                            } else {
+                                self.deleteLinkPreview()
+                            }
+                        }
+                    })
+                }
+            } else {
+                deleteLinkPreview()
             }
-        } else {
-            deleteLinkPreview()
         }
     }
     
@@ -1786,6 +1874,9 @@ extension EditorGroup: UITextViewDelegate {
         if !self.viewTextfield.subviews.contains(self.containerLink){
             UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
                 self.constraintTopTextField.constant = self.constraintTopTextField.constant + 80
+                if self.contraintBottomMention.constant > 0 {
+                    self.contraintBottomMention.constant = self.contraintBottomMention.constant + 80
+                }
             }, completion: nil)
         }
         
@@ -1869,7 +1960,7 @@ extension EditorGroup: UITextViewDelegate {
         cancelPreview.trailingAnchor.constraint(equalTo: self.containerLink.trailingAnchor, constant: -10).isActive = true
         cancelPreview.centerYAnchor.constraint(equalTo: self.containerLink.centerYAnchor).isActive = true
         cancelPreview.setImage(UIImage(systemName: "xmark.circle" , withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular, scale: .default)), for: .normal)
-        cancelPreview.addTarget(nil, action: #selector(self.deleteLinkPreview), for: .touchUpInside)
+        cancelPreview.addTarget(nil, action: #selector(self.removeLinkPreviewUntilEmptyTextView), for: .touchUpInside)
         cancelPreview.backgroundColor = .clear
         cancelPreview.tintColor = .mainColor
     }
@@ -1908,6 +1999,39 @@ extension EditorGroup: UITextViewDelegate {
     }
     
     public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
+        if text.isEmpty {
+            if listMentionInTextField.count > 0 {
+                for i in 0..<listMentionInTextField.count {
+                    if lastPositionCursorMention == Int(listMentionInTextField[i].ex_block!)! {
+                        let fulltextForMention = textFieldSend.text.substring(from: 0, to: lastPositionCursorMention - 1)
+                        let diff = textFieldSend.text.count - fulltextForMention.count
+                        var text = textView.text ?? ""
+                        let nameMention = (listMentionInTextField[i].firstName + " " + listMentionInTextField[i].lastName).trimmingCharacters(in: .whitespaces)
+                        let rangeReplacement = NSRange(location: lastPositionCursorMention - nameMention.count - 1, length: nameMention.count + 1)
+                        let replacementText = ""
+                        
+                        let copyAttributedText = text.richText(isEditing: true, group_id: self.dataGroup["group_id"] as! String, listMentionInTextField: listMentionInTextField)
+                        copyAttributedText.removeAttribute(.foregroundColor, range: rangeReplacement)
+                        
+                        textFieldSend.attributedText = copyAttributedText
+
+                        // Replace the old text with the new text using the replaceSubrange(_:with:) method
+                        if let startIndex = text.index(text.startIndex, offsetBy: rangeReplacement.location, limitedBy: text.endIndex),
+                           let endIndex = text.index(startIndex, offsetBy: rangeReplacement.length, limitedBy: text.endIndex) {
+                            text.replaceSubrange(startIndex..<endIndex, with: replacementText)
+                        }
+                        
+                        listMentionInTextField.remove(at: i)
+                        
+                        textFieldSend.attributedText = text.richText(isEditing: true, group_id: self.dataGroup["group_id"] as! String, listMentionInTextField: listMentionInTextField)
+                        
+                        let newPosition = textFieldSend.position(from: textFieldSend.beginningOfDocument, offset: textView.text.count - diff)
+                        textFieldSend.selectedTextRange = textFieldSend.textRange(from: newPosition!, to: newPosition!)
+                        return false
+                    }
+                }
+            }
+        }
         if (self.textFieldSend.text.count == 0) {
             return text != "\n"
         }
@@ -2485,10 +2609,18 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
             self.reffId = nil
             UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
                 self.constraintTopTextField.constant = self.constraintTopTextField.constant - 50
+                if self.contraintBottomMention.constant > 0 {
+                    self.contraintBottomMention.constant = self.contraintBottomMention.constant - 50
+                }
             }, completion: nil)
         }
     }
     
+    @objc func removeLinkPreviewUntilEmptyTextView() {
+        isAlwaysHideLinkPreview = true
+        deleteLinkPreview()
+    }
+    
     @objc func deleteLinkPreview() {
         if self.containerLink.isDescendant(of: self.viewTextfield) {
             self.containerLink.subviews.forEach { $0.removeFromSuperview() }
@@ -2496,6 +2628,9 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
             self.containerLink.removeFromSuperview()
             UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
                 self.constraintTopTextField.constant = self.constraintTopTextField.constant - 80
+                if self.contraintBottomMention.constant > 0 {
+                    self.contraintBottomMention.constant = self.contraintBottomMention.constant - 80
+                }
             }, completion: nil)
             self.showingLink = ""
         }
@@ -2624,7 +2759,37 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
     public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
         if tableView == tableMention {
             tableView.deselectRow(at: indexPath, animated: true)
-            
+            let fulltextForMention = textFieldSend.text.substring(from: 0, to: lastPositionCursorMention - 1)
+            let diff = textFieldSend.text.count - fulltextForMention.count
+            if let indexLastMention = fulltextForMention.lastIndex(of: "@") {
+                listMentionInTextField.append(listMentionWithText[indexPath.row])
+                let indexIntMention = fulltextForMention.distance(from: fulltextForMention.startIndex, to: indexLastMention)
+                let rangeReplacement = NSRange(location: indexIntMention, length: lastPositionCursorMention - indexIntMention)
+                
+                var addSpaceAfterReplacement = ""
+                if diff == 0 {
+                    addSpaceAfterReplacement = " "
+                }
+                
+                var text = textFieldSend.text ?? ""
+                let nameMention = (listMentionWithText[indexPath.row].firstName + " " + listMentionWithText[indexPath.row].lastName).trimmingCharacters(in: .whitespaces)
+                let replacementText = "@\(nameMention)"
+
+                // Replace the old text with the new text using the replaceSubrange(_:with:) method
+                if let startIndex = text.index(text.startIndex, offsetBy: rangeReplacement.location, limitedBy: text.endIndex),
+                   let endIndex = text.index(startIndex, offsetBy: rangeReplacement.length, limitedBy: text.endIndex) {
+                    text.replaceSubrange(startIndex..<endIndex, with: replacementText + addSpaceAfterReplacement)
+                }
+                
+                textFieldSend.attributedText = text.richText(isEditing: true, group_id: self.dataGroup["group_id"] as! String, listMentionInTextField: listMentionInTextField)
+                
+                let newPosition = textFieldSend.position(from: textFieldSend.beginningOfDocument, offset: textFieldSend.text.count - diff)
+                textFieldSend.selectedTextRange = textFieldSend.textRange(from: newPosition!, to: newPosition!)
+                
+                listMentionInTextField.last?.ex_block = "\(textFieldSend.text.count - diff - addSpaceAfterReplacement.count)" //upperBound
+                
+                hideMention()
+            }
             return
         }
         let dataMessages = self.dataMessages.filter({ $0["chat_date"] as! String == dataDates[indexPath.section] })
@@ -2702,10 +2867,12 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
             content.textProperties.font = UIFont.systemFont(ofSize: 11)
             content.imageProperties.tintColor = .black
             content.imageProperties.maximumSize = CGSize(width: 24, height: 24)
-            getImage(name: listMentionWithText[indexPath.row].thumb, placeholderImage: UIImage(systemName: "person"), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
-                content.image = image
-            })
-            content.text = listMentionWithText[indexPath.row].firstName
+            if indexPath.row < listMentionWithText.count {
+                getImage(name: listMentionWithText[indexPath.row].thumb, placeholderImage: UIImage(systemName: "person"), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
+                    content.image = image
+                })
+                content.text = listMentionWithText[indexPath.row].firstName
+            }
             cellMention.contentConfiguration = content
             return cellMention
         }
@@ -2727,11 +2894,6 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
         profileMessage.addGestureRecognizer(tapGestureRecognizer)
         
         let containerMessage = UIView()
-        if !copySession && !forwardSession && !deleteSession && !isHistoryCC && !removed {
-            let interaction = UIContextMenuInteraction(delegate: self)
-            containerMessage.addInteraction(interaction)
-            containerMessage.isUserInteractionEnabled = true
-        }
         
         let thumbChat = dataMessages[indexPath.row]["thumb_id"] as! String
         let imageChat = dataMessages[indexPath.row]["image_id"] as! String
@@ -3116,28 +3278,43 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                 imageSticker.contentMode = .scaleAspectFit
             }
             else {
-                messageText.attributedText = textChat!.richText()
+                messageText.attributedText = textChat!.richText(group_id: self.dataGroup["group_id"] as! String)
             }
         } else {
-            messageText.attributedText = textChat!.richText()
+            messageText.attributedText = textChat!.richText(group_id: self.dataGroup["group_id"] as! String)
         }
         
-        messageText.isUserInteractionEnabled = true
+        messageText.isUserInteractionEnabled = false
         if !textChat!.isEmpty {
-            let listText = textChat!.split(separator: " ")
-            for i in 0...listText.count - 1 {
-                if listText[i].lowercased().checkStartWithLink() {
-                    if ((listText[i].lowercased().starts(with: "www.") && listText[i].lowercased().split(separator: ".").count >= 3) || (!listText[i].lowercased().starts(with: "www.") && listText[i].lowercased().split(separator: ".").count >= 2)) && listText[i].lowercased().split(separator: ".").last!.count >= 2 {
-                        let objectGesture = ObjectGesture(target: self, action: #selector(tapMessageText(_:)))
-                        objectGesture.message_id = "\(listText[i])"
-                        messageText.addGestureRecognizer(objectGesture)
+            let listTextEnter = textChat!.split(separator: "\n")
+            for j in 0...listTextEnter.count - 1 {
+                let listText = listTextEnter[j].split(separator: " ")
+                for i in 0...listText.count - 1 {
+                    if listText[i].lowercased().checkStartWithLink() {
+                        let rangeTapLink = (textChat! as NSString).range(of: String(listText[i]))
+                        // Add tap gesture recognizer to the range of text
+                        let attributedString = textChat!.richText(group_id: self.dataGroup["group_id"] as! String)
+                        attributedString.addAttributes([.foregroundColor: UIColor.blue, .underlineStyle: NSUnderlineStyle.single.rawValue], range: rangeTapLink)
+                        messageText.attributedText = attributedString
+                        if messageText.isUserInteractionEnabled == false && !copySession && !forwardSession && !deleteSession && !isHistoryCC && !removed {
+                            messageText.isUserInteractionEnabled = true
+                            let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressLink(_:)))
+                            longPress.minimumPressDuration = 0.1
+                            containerMessage.addGestureRecognizer(longPress)
+                        }
                     }
                 }
             }
         }
         
+        if !copySession && !forwardSession && !deleteSession && !isHistoryCC && !removed && messageText.isUserInteractionEnabled == false {
+            let interaction = UIContextMenuInteraction(delegate: self)
+            containerMessage.addInteraction(interaction)
+            containerMessage.isUserInteractionEnabled = true
+        }
+        
         if isSearching && textSearch.count > 1 {
-            messageText.attributedText = textChat!.richText(isSearching: true, textSearch: textSearch)
+            messageText.attributedText = textChat!.richText(isSearching: true, textSearch: textSearch, group_id: self.dataGroup["group_id"] as! String)
             if textChat!.lowercased().contains(textSearch) {
                 countMatchesSearch += 1
             }
@@ -3368,6 +3545,120 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
             }
         }
         
+        let containerLinkMessage = UIView()
+        if thumbChat.isEmpty && fileChat.isEmpty && !textChat!.isEmpty {
+            var text = ""
+            let listTextSplitBreak = textChat!.components(separatedBy: "\n")
+            let indexFirstLinkSplitBreak = listTextSplitBreak.firstIndex(where: { $0.contains("www.") || $0.contains("http://") || $0.contains("https://") })
+            if indexFirstLinkSplitBreak != nil {
+                let listTextSplitSpace = listTextSplitBreak[indexFirstLinkSplitBreak!].components(separatedBy: " ")
+                let indexFirstLinkSplitSpace = listTextSplitSpace.firstIndex(where: { ($0.starts(with: "www.") && $0.components(separatedBy: ".").count > 2) || ($0.starts(with: "http://") && $0.components(separatedBy: ".").count > 1) || ($0.starts(with: "https://") && $0.components(separatedBy: ".").count > 1) })
+                if indexFirstLinkSplitSpace != nil {
+                    text = listTextSplitSpace[indexFirstLinkSplitSpace!]
+                }
+            }
+            if !text.isEmpty {
+                var dataURL = ""
+                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                    if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select data_link from LINK_PREVIEW where link='\(text)'"), cursor.next() {
+                        if let data = cursor.string(forColumnIndex: 0) {
+                            dataURL = data
+                        }
+                        cursor.close()
+                    }
+                })
+                
+                if !dataURL.isEmpty {
+                    if let data = try! JSONSerialization.jsonObject(with: dataURL.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
+                        let title = data["title"] as! String
+                        let description = data["description"] as! String
+                        let imageUrl = data["imageUrl"] as? String
+                        let link = data["link"] as! String
+                        
+                        topMarginText.constant = topMarginText.constant + 80
+                        
+                        containerMessage.addSubview(containerLinkMessage)
+                        containerLinkMessage.translatesAutoresizingMaskIntoConstraints = false
+                        containerLinkMessage.leadingAnchor.constraint(equalTo:containerMessage.leadingAnchor, constant: 15).isActive = true
+                        if dataMessages[indexPath.row]["attachment_flag"] as? String == "11" {
+                            containerLinkMessage.bottomAnchor.constraint(equalTo: imageSticker.topAnchor, constant: -5).isActive = true
+                        } else {
+                            containerLinkMessage.bottomAnchor.constraint(equalTo: messageText.topAnchor, constant: -5).isActive = true
+                        }
+                        containerLinkMessage.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
+                        containerLinkMessage.heightAnchor.constraint(equalToConstant: 80.0).isActive = true
+                        containerLinkMessage.backgroundColor = .gray.withAlphaComponent(0.2)
+                        
+                        let imagePreview = UIImageView()
+                        if imageUrl != nil {
+                            containerLinkMessage.addSubview(imagePreview)
+                            imagePreview.translatesAutoresizingMaskIntoConstraints = false
+                            imagePreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor).isActive = true
+                            imagePreview.bottomAnchor.constraint(equalTo: containerLinkMessage.bottomAnchor).isActive = true
+                            imagePreview.topAnchor.constraint(equalTo: containerLinkMessage.topAnchor).isActive = true
+                            imagePreview.widthAnchor.constraint(equalToConstant: 80.0).isActive = true
+                            if !imageUrl!.starts(with: "https://") {
+                                imagePreview.loadImageAsync(with: "https://www.google.be" + imageUrl!)
+                            } else {
+                                imagePreview.loadImageAsync(with: imageUrl)
+                            }
+                            imagePreview.contentMode = .scaleAspectFit
+                        }
+                        
+                        let titlePreview = UILabel()
+                        containerLinkMessage.addSubview(titlePreview)
+                        titlePreview.translatesAutoresizingMaskIntoConstraints = false
+                        if imageUrl != nil {
+                            titlePreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
+                        } else {
+                            titlePreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor, constant: 5.0).isActive = true
+                        }
+                        titlePreview.topAnchor.constraint(equalTo: containerLinkMessage.topAnchor, constant: 25.0).isActive = true
+                        titlePreview.trailingAnchor.constraint(equalTo: containerLinkMessage.trailingAnchor, constant: -80.0).isActive = true
+                        titlePreview.text = title
+                        titlePreview.font = UIFont.systemFont(ofSize: 14.0, weight: .bold)
+                        titlePreview.textColor = .black
+                        
+                        let descPreview = UILabel()
+                        containerLinkMessage.addSubview(descPreview)
+                        descPreview.translatesAutoresizingMaskIntoConstraints = false
+                        if imageUrl != nil {
+                            descPreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
+                        } else {
+                            descPreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor, constant: 5.0).isActive = true
+                        }
+                        descPreview.topAnchor.constraint(equalTo: titlePreview.bottomAnchor).isActive = true
+                        descPreview.trailingAnchor.constraint(equalTo: containerLinkMessage.trailingAnchor, constant: -80.0).isActive = true
+                        descPreview.text = description
+                        descPreview.font = UIFont.systemFont(ofSize: 12.0)
+                        descPreview.textColor = .gray
+                        descPreview.numberOfLines = 1
+                        
+                        let linkPreview = UILabel()
+                        containerLinkMessage.addSubview(linkPreview)
+                        linkPreview.translatesAutoresizingMaskIntoConstraints = false
+                        if imageUrl != nil {
+                            linkPreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
+                        } else {
+                            linkPreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor, constant: 5.0).isActive = true
+                        }
+                        linkPreview.topAnchor.constraint(equalTo: descPreview.bottomAnchor, constant: 8.0).isActive = true
+                        linkPreview.trailingAnchor.constraint(equalTo: containerLinkMessage.trailingAnchor, constant: -80.0).isActive = true
+                        linkPreview.text = link
+                        linkPreview.font = UIFont.systemFont(ofSize: 10.0)
+                        linkPreview.textColor = .gray
+                        linkPreview.numberOfLines = 1
+                        
+                        if !copySession && !forwardSession && !deleteSession {
+                            let objectTap = ObjectGesture(target: self, action: #selector(tapMessageText(_:)))
+                            objectTap.message_id = text
+                            containerLinkMessage.addGestureRecognizer(objectTap)
+                        }
+                    }
+                }
+            }
+        }
+        
         if (reffChat != "") {
             let data = queryMessageReply(message_id: reffChat)
             if data.count != 0 {
@@ -3382,6 +3673,8 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                     containerReply.bottomAnchor.constraint(equalTo: imageThumb.topAnchor, constant: -5).isActive = true
                 } else if fileChat != "" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as! String != "1") {
                     containerReply.bottomAnchor.constraint(equalTo: containerViewFile.topAnchor, constant: -5).isActive = true
+                } else if containerMessage.subviews.contains(containerLinkMessage) {
+                    containerReply.bottomAnchor.constraint(equalTo: containerLinkMessage.topAnchor, constant: -5).isActive = true
                 } else if dataMessages[indexPath.row]["attachment_flag"] as? String == "11" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as! String != "1") {
                     containerReply.bottomAnchor.constraint(equalTo: imageSticker.topAnchor, constant: -5).isActive = true
                 } else {
@@ -3450,18 +3743,18 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                 let file_chat = data["file_id"] as! String
                 if (attachment_flag == "0" && thumb_chat == "") {
                     contentReply.trailingAnchor.constraint(equalTo: containerReply.trailingAnchor, constant: -20).isActive = true
-                    contentReply.attributedText = message_text.richText()
+                    contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as! String)
                 } else if (attachment_flag == "1" || image_chat != "") {
                     if (message_text == "") {
                         contentReply.text = "📷 Photo".localized()
                     } else {
-                        contentReply.attributedText = message_text.richText()
+                        contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as! String)
                     }
                 } else if (attachment_flag == "2" || video_chat != "") {
                     if (message_text == "") {
                         contentReply.text = "📹 Video".localized()
                     } else {
-                        contentReply.attributedText = message_text.richText()
+                        contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as! String)
                     }
                 } else if (attachment_flag == "6" || file_chat != ""){
                     contentReply.trailingAnchor.constraint(equalTo: containerReply.trailingAnchor, constant: -20).isActive = true
@@ -3798,6 +4091,118 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
             }
         }
     }
+    
+    @objc func handleLongPressLink(_ gestureRecognizer: UILongPressGestureRecognizer) {
+        func showMenuContext() {
+            if gestureRecognizer.state == .cancelled || gestureRecognizer.state == .ended{
+                timerCheckLink?.invalidate()
+            } else if gestureRecognizer.state == .began {
+                timerCheckLink = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false, block: {_ in
+                    let interaction = UIContextMenuInteraction(delegate: self)
+                    gestureRecognizer.view!.addInteraction(interaction)
+                    guard let interaction = gestureRecognizer.view!.interactions.first,
+                          let data = Data(base64Encoded: "X3ByZXNlbnRNZW51QXRMb2NhdGlvbjo="),
+                          let str = String(data: data, encoding: .utf8)
+                    else {
+                        return
+                    }
+                    let selector = NSSelectorFromString(str)
+                    guard interaction.responds(to: selector) else {
+                        return
+                    }
+                    let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                    impactHeavy.impactOccurred()
+                    interaction.perform(selector, with: self.view)
+                    self.isLinkCopied = true
+                })
+            }
+        }
+        if gestureRecognizer.state == .began {
+            let touchPoint = gestureRecognizer.location(in: self.view)
+            touchedSubview = self.view.hitTest(touchPoint, with: nil) ?? UIView()
+            if !(touchedSubview is UILabel) {
+                showMenuContext()
+            }
+        }
+        guard let label = touchedSubview as? UILabel else { return }
+        let touchPointLabel = gestureRecognizer.location(in: label)
+
+        if let text = label.text, let range = getWordRange(at: touchPointLabel, in: label) {
+            let word = String(text[range])
+            if word.starts(with: "www.") || word.starts(with: "https://") || word.starts(with: "http://") {
+                if gestureRecognizer.state == .cancelled || gestureRecognizer.state == .ended{
+                    timerCheckLink?.invalidate()
+                    if !isLinkCopied {
+                        var stringURl = word
+                        if stringURl.starts(with: "www.") {
+                            stringURl = "https://" + stringURl.replacingOccurrences(of: "www.", with: "")
+                        }
+                        guard let url = URL(string: stringURl) else { return }
+                        UIApplication.shared.open(url)
+                        label.attributedText = removeHighlightedText(for: text, in: range, label: label)
+                    }
+                    isLinkCopied = false
+                } else if gestureRecognizer.state == .began {
+                    label.attributedText = highlightedText(for: text, in: range, label: label)
+                    timerCheckLink = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: {_ in
+                        self.isLinkCopied = true
+                        UIPasteboard.general.string = word
+                        self.showToast(message: "Link Copied".localized(), font: UIFont.systemFont(ofSize: 12, weight: .medium), controller: self)
+                        label.attributedText = self.removeHighlightedText(for: text, in: range, label: label)
+                        self.timerCheckLink?.invalidate()
+                    })
+                }
+            } else {
+                showMenuContext()
+            }
+        } else {
+            showMenuContext()
+        }
+    }
+    
+    func getWordRange(at point: CGPoint, in label: UILabel) -> Range<String.Index>? {
+        guard let text = label.text else { return nil }
+        
+        let layoutManager = NSLayoutManager()
+        let textContainer = NSTextContainer(size: label.frame.size)
+        let textStorage = NSTextStorage(attributedString: NSAttributedString(string: text))
+        
+        layoutManager.addTextContainer(textContainer)
+        textStorage.addLayoutManager(layoutManager)
+        textContainer.lineFragmentPadding = 0
+        textContainer.maximumNumberOfLines = label.numberOfLines
+        
+        lastTouchPoint = point
+        let characterIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
+        
+        if characterIndex == text.count - 1 {
+            return nil
+        }
+        var wordStartIndex = characterIndex
+        while wordStartIndex > 0 && text[text.index(text.startIndex, offsetBy: wordStartIndex - 1)] != " " && text[text.index(text.startIndex, offsetBy: wordStartIndex - 1)] != "\n" {
+            wordStartIndex -= 1
+        }
+        
+        var wordEndIndex = characterIndex
+        while wordEndIndex < text.count && text[text.index(text.startIndex, offsetBy: wordEndIndex)] != " " && text[text.index(text.startIndex, offsetBy: wordEndIndex)] != "\n" {
+            wordEndIndex += 1
+        }
+        
+        return text.index(text.startIndex, offsetBy: wordStartIndex)..<text.index(text.startIndex, offsetBy: wordEndIndex)
+    }
+
+    func highlightedText(for text: String, in range: Range<String.Index>, label: UILabel) -> NSAttributedString {
+        let mutableAttributedString = label.attributedText!.mutableCopy() as! NSMutableAttributedString
+        mutableAttributedString.addAttribute(.backgroundColor, value: UIColor.lightGray.withAlphaComponent(0.5), range: NSRange(range, in: text))
+        return mutableAttributedString
+    }
+    
+    func removeHighlightedText(for text: String, in range: Range<String.Index>, label: UILabel) -> NSAttributedString {
+        let mutableAttributedString = label.attributedText!.mutableCopy() as! NSMutableAttributedString
+        mutableAttributedString.removeAttribute(.backgroundColor, range: NSRange(range, in: text))
+        return mutableAttributedString
+    }
+    
     @objc func tapMessageText(_ sender: ObjectGesture) {
         var stringURl = sender.message_id.lowercased()
         if stringURl.starts(with: "www.") {
@@ -3840,6 +4245,9 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
         self.reffId = dataMessages[indexPath.row]["message_id"] as? String
         UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
             self.constraintTopTextField.constant = self.constraintTopTextField.constant + 50
+            if self.contraintBottomMention.constant > 0 {
+                self.contraintBottomMention.constant = self.contraintBottomMention.constant + 50
+            }
         }, completion: nil)
         if (self.currentIndexpath != nil) {
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
@@ -3903,18 +4311,18 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
         let video_chat = dataMessages[indexPath.row]["video_id"] as! String
         let file_chat = dataMessages[indexPath.row]["file_id"] as! String
         if (attachment_flag == "0" && thumb_chat == "") {
-            contentReply.attributedText = message_text.richText()
+            contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as! String)
         } else if (attachment_flag == "1" || image_chat != "") {
             if (message_text == "") {
                 contentReply.text = "📷 Photo".localized()
             } else {
-                contentReply.attributedText = message_text.richText()
+                contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as! String)
             }
         } else if (attachment_flag == "2" || video_chat != "") {
             if (message_text == "") {
                 contentReply.text = "📹 Video".localized()
             } else {
-                contentReply.attributedText = message_text.richText()
+                contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as! String)
             }
         } else if (attachment_flag == "6" || file_chat != ""){
             contentReply.text = "📄 \(message_text.components(separatedBy: "|")[0])"

+ 342 - 81
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -93,6 +93,12 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     var lastOffsetDown = 1
     var gettingDataMessage = true
     var showingLink = ""
+    var isAlwaysHideLinkPreview = false
+    var timerCheckLink: Timer?
+    var timerLongPressLink: Timer?
+    var lastTouchPoint: CGPoint = .zero
+    var isLinkCopied = false
+    var touchedSubview = UIView()
     
     public override func viewDidDisappear(_ animated: Bool) {
         if self.isMovingFromParent {
@@ -2865,6 +2871,9 @@ extension EditorPersonal: UITextViewDelegate {
     }
     
     public func textViewDidChange(_ textView: UITextView) {
+        if textView.text.count == 0 {
+            isAlwaysHideLinkPreview = false
+        }
         if allowTyping {
             allowTyping = false
             if isContactCenter && !fPinContacCenter.isEmpty {
@@ -2876,7 +2885,10 @@ extension EditorPersonal: UITextViewDelegate {
                 self.allowTyping = true
             })
         }
-        checkLink(fullText: textView.text)
+        timerCheckLink?.invalidate()
+        timerCheckLink = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: {_ in
+            self.checkLink(fullText: textView.text)
+        })
         if textView.text.contains("*") || textView.text.contains("_") || textView.text.contains("^") || textView.text.contains("~") {
             textView.preserveCursorPosition(withChanges: { _ in
                 textView.attributedText = textView.text.richText(isEditing: true)
@@ -2886,84 +2898,86 @@ extension EditorPersonal: UITextViewDelegate {
     }
     
     private func checkLink(fullText: String) {
-        var text = ""
-        let listTextSplitBreak = fullText.components(separatedBy: "\n")
-        let indexFirstLinkSplitBreak = listTextSplitBreak.firstIndex(where: { $0.contains("www.") || $0.contains("http://") || $0.contains("https://") })
-        if indexFirstLinkSplitBreak != nil {
-            let listTextSplitSpace = listTextSplitBreak[indexFirstLinkSplitBreak!].components(separatedBy: " ")
-            let indexFirstLinkSplitSpace = listTextSplitSpace.firstIndex(where: { ($0.starts(with: "www.") && $0.components(separatedBy: ".").count > 2) || ($0.starts(with: "http://") && $0.components(separatedBy: ".").count > 1) || ($0.starts(with: "https://") && $0.components(separatedBy: ".").count > 1) })
-            if indexFirstLinkSplitSpace != nil {
-                text = listTextSplitSpace[indexFirstLinkSplitSpace!]
-            }
-        }
-        if !text.isEmpty {
-            var stringURl = text
-            if stringURl.starts(with: "www.") {
-                stringURl = "https://" + stringURl.replacingOccurrences(of: "www.", with: "")
-            }
-            var dataURL = ""
-            Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select data_link from LINK_PREVIEW where link='\(text)'") {
-                    while cursor.next() {
-                        if let data = cursor.string(forColumnIndex: 0) {
-                            dataURL = data
-                        }
-                    }
-                    cursor.close()
+        if !isAlwaysHideLinkPreview {
+            var text = ""
+            let listTextSplitBreak = fullText.components(separatedBy: "\n")
+            let indexFirstLinkSplitBreak = listTextSplitBreak.firstIndex(where: { $0.contains("www.") || $0.contains("http://") || $0.contains("https://") })
+            if indexFirstLinkSplitBreak != nil {
+                let listTextSplitSpace = listTextSplitBreak[indexFirstLinkSplitBreak!].components(separatedBy: " ")
+                let indexFirstLinkSplitSpace = listTextSplitSpace.firstIndex(where: { ($0.starts(with: "www.") && $0.components(separatedBy: ".").count > 2) || ($0.starts(with: "http://") && $0.components(separatedBy: ".").count > 1) || ($0.starts(with: "https://") && $0.components(separatedBy: ".").count > 1) })
+                if indexFirstLinkSplitSpace != nil {
+                    text = listTextSplitSpace[indexFirstLinkSplitSpace!]
                 }
-            })
-            if !dataURL.isEmpty {
-                if let data = try! JSONSerialization.jsonObject(with: dataURL.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
-                    let title = data["title"] as! String
-                    let description = data["description"] as! String
-                    let imageUrl = data["imageUrl"] as? String
-                    let link = data["link"] as! String
-                    if self.showingLink != text {
-                        self.showingLink = text
-                        self.deleteLinkPreview()
-                        self.buildPreviewLink(imageUrl: imageUrl, title: title, description: description, stringURl: link)
-                    }
+            }
+            if !text.isEmpty {
+                var stringURl = text
+                if stringURl.starts(with: "www.") {
+                    stringURl = "https://" + stringURl.replacingOccurrences(of: "www.", with: "")
                 }
-            } else {
-                let urlData = URL(string: stringURl)!
-                Readability.parse(url: urlData, completion: { data in
-                    if data != nil {
-                        let title = data!.title
-                        let description = data!.description
-                        let imageUrl = data!.topImage
-                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                            do {
-                                var dataJson: [String: Any] = [:]
-                                dataJson["title"] = title
-                                dataJson["description"] = description
-                                dataJson["imageUrl"] = imageUrl
-                                dataJson["link"] = text
-                                guard let json = String(data: try! JSONSerialization.data(withJSONObject: dataJson, options: []), encoding: String.Encoding.utf8) else {
-                                    return
-                                }
-                                _ = try Database.shared.insertRecord(fmdb: fmdb, table: "LINK_PREVIEW", cvalues: [
-                                    "id" : "\(Date().currentTimeMillis().toHex())",
-                                    "link" : text,
-                                    "data_link" : json,
-                                    "retry": 0
-                                ], replace: true)
-                            } catch {
-                                rollback.pointee = true
-                                print(error)
+                var dataURL = ""
+                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                    if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select data_link from LINK_PREVIEW where link='\(text)'") {
+                        while cursor.next() {
+                            if let data = cursor.string(forColumnIndex: 0) {
+                                dataURL = data
                             }
-                        })
+                        }
+                        cursor.close()
+                    }
+                })
+                if !dataURL.isEmpty {
+                    if let data = try! JSONSerialization.jsonObject(with: dataURL.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
+                        let title = data["title"] as! String
+                        let description = data["description"] as! String
+                        let imageUrl = data["imageUrl"] as? String
+                        let link = data["link"] as! String
                         if self.showingLink != text {
                             self.showingLink = text
                             self.deleteLinkPreview()
-                            self.buildPreviewLink(imageUrl: imageUrl, title: title, description: description, stringURl: text)
+                            self.buildPreviewLink(imageUrl: imageUrl, title: title, description: description, stringURl: link)
                         }
-                    } else {
-                        self.deleteLinkPreview()
                     }
-                })
+                } else {
+                    let urlData = URL(string: stringURl)!
+                    Readability.parse(url: urlData, completion: { data in
+                        if data != nil {
+                            let title = data!.title
+                            let description = data!.description
+                            let imageUrl = data!.topImage
+                            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                do {
+                                    var dataJson: [String: Any] = [:]
+                                    dataJson["title"] = title
+                                    dataJson["description"] = description
+                                    dataJson["imageUrl"] = imageUrl
+                                    dataJson["link"] = text
+                                    guard let json = String(data: try! JSONSerialization.data(withJSONObject: dataJson, options: []), encoding: String.Encoding.utf8) else {
+                                        return
+                                    }
+                                    _ = try Database.shared.insertRecord(fmdb: fmdb, table: "LINK_PREVIEW", cvalues: [
+                                        "id" : "\(Date().currentTimeMillis().toHex())",
+                                        "link" : text,
+                                        "data_link" : json,
+                                        "retry": 0
+                                    ], replace: true)
+                                } catch {
+                                    rollback.pointee = true
+                                    print(error)
+                                }
+                            })
+                            if self.showingLink != text {
+                                self.showingLink = text
+                                self.deleteLinkPreview()
+                                self.buildPreviewLink(imageUrl: imageUrl, title: title, description: description, stringURl: text)
+                            }
+                        } else {
+                            self.deleteLinkPreview()
+                        }
+                    })
+                }
+            } else {
+                deleteLinkPreview()
             }
-        } else {
-            deleteLinkPreview()
         }
     }
     
@@ -3054,7 +3068,7 @@ extension EditorPersonal: UITextViewDelegate {
         cancelPreview.trailingAnchor.constraint(equalTo: self.containerLink.trailingAnchor, constant: -10).isActive = true
         cancelPreview.centerYAnchor.constraint(equalTo: self.containerLink.centerYAnchor).isActive = true
         cancelPreview.setImage(UIImage(systemName: "xmark.circle" , withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular, scale: .default)), for: .normal)
-        cancelPreview.addTarget(nil, action: #selector(self.deleteLinkPreview), for: .touchUpInside)
+        cancelPreview.addTarget(nil, action: #selector(self.removeLinkPreviewUntilEmptyTextView), for: .touchUpInside)
         cancelPreview.backgroundColor = .clear
         cancelPreview.tintColor = .mainColor
     }
@@ -3105,6 +3119,13 @@ extension EditorPersonal: UITextViewDelegate {
 
 //EUC
 extension EditorPersonal: UIContextMenuInteractionDelegate {
+    public func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
+        if isLinkCopied {
+            isLinkCopied = false
+            interaction.view!.removeInteraction(interaction)
+        }
+    }
+    
     public func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
         if textFieldSend.isFirstResponder {
             textFieldSend.resignFirstResponder()
@@ -3738,6 +3759,11 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
         }
     }
     
+    @objc func removeLinkPreviewUntilEmptyTextView() {
+        isAlwaysHideLinkPreview = true
+        deleteLinkPreview()
+    }
+    
     @objc func deleteLinkPreview() {
         if self.containerLink.isDescendant(of: self.viewTextfield) {
             self.containerLink.subviews.forEach { $0.removeFromSuperview() }
@@ -4152,11 +4178,6 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
         }
         
         let containerMessage = UIView()
-        if !copySession && !forwardSession && !deleteSession && !self.removed {
-            let interaction = UIContextMenuInteraction(delegate: self)
-            containerMessage.addInteraction(interaction)
-            containerMessage.isUserInteractionEnabled = true
-        }
         cell.contentView.addSubview(containerMessage)
         containerMessage.translatesAutoresizingMaskIntoConstraints = false
         
@@ -4509,23 +4530,36 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
         } else {
             messageText.attributedText = textChat.richText()
         }
-        messageText.isUserInteractionEnabled = true
+        
+        messageText.isUserInteractionEnabled = false
         if !textChat.isEmpty {
             let listTextEnter = textChat.split(separator: "\n")
             for j in 0...listTextEnter.count - 1 {
                 let listText = listTextEnter[j].split(separator: " ")
                 for i in 0...listText.count - 1 {
                     if listText[i].lowercased().checkStartWithLink() {
-                        if ((listText[i].lowercased().starts(with: "www.") && listText[i].lowercased().split(separator: ".").count >= 3) || (!listText[i].lowercased().starts(with: "www.") && listText[i].lowercased().split(separator: ".").count >= 2)) && listText[i].lowercased().split(separator: ".").last!.count >= 2 {
-                            let objectGesture = ObjectGesture(target: self, action: #selector(tapMessageText(_:)))
-                            objectGesture.message_id = "\(listText[i])"
-                            messageText.addGestureRecognizer(objectGesture)
+                        let rangeTapLink = (textChat as NSString).range(of: String(listText[i]))
+                        // Add tap gesture recognizer to the range of text
+                        let attributedString = textChat.richText()
+                        attributedString.addAttributes([.foregroundColor: UIColor.blue, .underlineStyle: NSUnderlineStyle.single.rawValue], range: rangeTapLink)
+                        messageText.attributedText = attributedString
+                        if messageText.isUserInteractionEnabled == false && !copySession && !forwardSession && !deleteSession && !self.removed {
+                            messageText.isUserInteractionEnabled = true
+                            let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressLink(_:)))
+                            longPress.minimumPressDuration = 0.1
+                            containerMessage.addGestureRecognizer(longPress)
                         }
                     }
                 }
             }
         }
         
+        if !copySession && !forwardSession && !deleteSession && !self.removed && messageText.isUserInteractionEnabled == false {
+            let interaction = UIContextMenuInteraction(delegate: self)
+            containerMessage.addInteraction(interaction)
+            containerMessage.isUserInteractionEnabled = true
+        }
+        
         if isSearching && textSearch.count > 1 {
             messageText.attributedText = textChat.richText(isSearching: true, textSearch: textSearch)
             if textChat.lowercased().contains(textSearch) {
@@ -4767,6 +4801,120 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
             }
         }
         
+        let containerLinkMessage = UIView()
+        if thumbChat.isEmpty && fileChat.isEmpty && !textChat.isEmpty {
+            var text = ""
+            let listTextSplitBreak = textChat.components(separatedBy: "\n")
+            let indexFirstLinkSplitBreak = listTextSplitBreak.firstIndex(where: { $0.contains("www.") || $0.contains("http://") || $0.contains("https://") })
+            if indexFirstLinkSplitBreak != nil {
+                let listTextSplitSpace = listTextSplitBreak[indexFirstLinkSplitBreak!].components(separatedBy: " ")
+                let indexFirstLinkSplitSpace = listTextSplitSpace.firstIndex(where: { ($0.starts(with: "www.") && $0.components(separatedBy: ".").count > 2) || ($0.starts(with: "http://") && $0.components(separatedBy: ".").count > 1) || ($0.starts(with: "https://") && $0.components(separatedBy: ".").count > 1) })
+                if indexFirstLinkSplitSpace != nil {
+                    text = listTextSplitSpace[indexFirstLinkSplitSpace!]
+                }
+            }
+            if !text.isEmpty {
+                var dataURL = ""
+                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                    if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select data_link from LINK_PREVIEW where link='\(text)'"), cursor.next() {
+                        if let data = cursor.string(forColumnIndex: 0) {
+                            dataURL = data
+                        }
+                        cursor.close()
+                    }
+                })
+                
+                if !dataURL.isEmpty {
+                    if let data = try! JSONSerialization.jsonObject(with: dataURL.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
+                        let title = data["title"] as! String
+                        let description = data["description"] as! String
+                        let imageUrl = data["imageUrl"] as? String
+                        let link = data["link"] as! String
+                        
+                        topMarginText.constant = topMarginText.constant + 80
+                        
+                        containerMessage.addSubview(containerLinkMessage)
+                        containerLinkMessage.translatesAutoresizingMaskIntoConstraints = false
+                        containerLinkMessage.leadingAnchor.constraint(equalTo:containerMessage.leadingAnchor, constant: 15).isActive = true
+                        if dataMessages[indexPath.row]["attachment_flag"] as? String == "11" {
+                            containerLinkMessage.bottomAnchor.constraint(equalTo: imageSticker.topAnchor, constant: -5).isActive = true
+                        } else {
+                            containerLinkMessage.bottomAnchor.constraint(equalTo: messageText.topAnchor, constant: -5).isActive = true
+                        }
+                        containerLinkMessage.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
+                        containerLinkMessage.heightAnchor.constraint(equalToConstant: 80.0).isActive = true
+                        containerLinkMessage.backgroundColor = .gray.withAlphaComponent(0.2)
+                        
+                        let imagePreview = UIImageView()
+                        if imageUrl != nil {
+                            containerLinkMessage.addSubview(imagePreview)
+                            imagePreview.translatesAutoresizingMaskIntoConstraints = false
+                            imagePreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor).isActive = true
+                            imagePreview.bottomAnchor.constraint(equalTo: containerLinkMessage.bottomAnchor).isActive = true
+                            imagePreview.topAnchor.constraint(equalTo: containerLinkMessage.topAnchor).isActive = true
+                            imagePreview.widthAnchor.constraint(equalToConstant: 80.0).isActive = true
+                            if !imageUrl!.starts(with: "https://") {
+                                imagePreview.loadImageAsync(with: "https://www.google.be" + imageUrl!)
+                            } else {
+                                imagePreview.loadImageAsync(with: imageUrl)
+                            }
+                            imagePreview.contentMode = .scaleAspectFit
+                        }
+                        
+                        let titlePreview = UILabel()
+                        containerLinkMessage.addSubview(titlePreview)
+                        titlePreview.translatesAutoresizingMaskIntoConstraints = false
+                        if imageUrl != nil {
+                            titlePreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
+                        } else {
+                            titlePreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor, constant: 5.0).isActive = true
+                        }
+                        titlePreview.topAnchor.constraint(equalTo: containerLinkMessage.topAnchor, constant: 25.0).isActive = true
+                        titlePreview.trailingAnchor.constraint(equalTo: containerLinkMessage.trailingAnchor, constant: -80.0).isActive = true
+                        titlePreview.text = title
+                        titlePreview.font = UIFont.systemFont(ofSize: 14.0, weight: .bold)
+                        titlePreview.textColor = .black
+                        
+                        let descPreview = UILabel()
+                        containerLinkMessage.addSubview(descPreview)
+                        descPreview.translatesAutoresizingMaskIntoConstraints = false
+                        if imageUrl != nil {
+                            descPreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
+                        } else {
+                            descPreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor, constant: 5.0).isActive = true
+                        }
+                        descPreview.topAnchor.constraint(equalTo: titlePreview.bottomAnchor).isActive = true
+                        descPreview.trailingAnchor.constraint(equalTo: containerLinkMessage.trailingAnchor, constant: -80.0).isActive = true
+                        descPreview.text = description
+                        descPreview.font = UIFont.systemFont(ofSize: 12.0)
+                        descPreview.textColor = .gray
+                        descPreview.numberOfLines = 1
+                        
+                        let linkPreview = UILabel()
+                        containerLinkMessage.addSubview(linkPreview)
+                        linkPreview.translatesAutoresizingMaskIntoConstraints = false
+                        if imageUrl != nil {
+                            linkPreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
+                        } else {
+                            linkPreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor, constant: 5.0).isActive = true
+                        }
+                        linkPreview.topAnchor.constraint(equalTo: descPreview.bottomAnchor, constant: 8.0).isActive = true
+                        linkPreview.trailingAnchor.constraint(equalTo: containerLinkMessage.trailingAnchor, constant: -80.0).isActive = true
+                        linkPreview.text = link
+                        linkPreview.font = UIFont.systemFont(ofSize: 10.0)
+                        linkPreview.textColor = .gray
+                        linkPreview.numberOfLines = 1
+                        
+                        if !copySession && !forwardSession && !deleteSession {
+                            let objectTap = ObjectGesture(target: self, action: #selector(tapMessageText(_:)))
+                            objectTap.message_id = text
+                            containerLinkMessage.addGestureRecognizer(objectTap)
+                        }
+                    }
+                }
+            }
+        }
+        
         if (reffChat != "" && dataMessages[indexPath.row]["message_scope_id"] as! String != "18") {
             let data = queryMessageReply(message_id: reffChat)
             if data.count != 0 {
@@ -4781,6 +4929,8 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
                     containerReply.bottomAnchor.constraint(equalTo: imageThumb.topAnchor, constant: -5).isActive = true
                 } else if fileChat != "" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as! String != "1") {
                     containerReply.bottomAnchor.constraint(equalTo: containerViewFile.topAnchor, constant: -5).isActive = true
+                } else if containerMessage.subviews.contains(containerLinkMessage) {
+                    containerReply.bottomAnchor.constraint(equalTo: containerLinkMessage.topAnchor, constant: -5).isActive = true
                 } else if dataMessages[indexPath.row]["attachment_flag"] as? String == "11" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as! String != "1") {
                     containerReply.bottomAnchor.constraint(equalTo: imageSticker.topAnchor, constant: -5).isActive = true
                 } else {
@@ -5256,6 +5406,117 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
             }
         }
     }
+    
+    @objc func handleLongPressLink(_ gestureRecognizer: UILongPressGestureRecognizer) {
+        func showMenuContext() {
+            if gestureRecognizer.state == .cancelled || gestureRecognizer.state == .ended{
+                timerCheckLink?.invalidate()
+            } else if gestureRecognizer.state == .began {
+                timerCheckLink = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false, block: {_ in
+                    let interaction = UIContextMenuInteraction(delegate: self)
+                    gestureRecognizer.view!.addInteraction(interaction)
+                    guard let interaction = gestureRecognizer.view!.interactions.first,
+                          let data = Data(base64Encoded: "X3ByZXNlbnRNZW51QXRMb2NhdGlvbjo="),
+                          let str = String(data: data, encoding: .utf8)
+                    else {
+                        return
+                    }
+                    let selector = NSSelectorFromString(str)
+                    guard interaction.responds(to: selector) else {
+                        return
+                    }
+                    let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                    impactHeavy.impactOccurred()
+                    interaction.perform(selector, with: self.view)
+                    self.isLinkCopied = true
+                })
+            }
+        }
+        if gestureRecognizer.state == .began {
+            let touchPoint = gestureRecognizer.location(in: self.view)
+            touchedSubview = self.view.hitTest(touchPoint, with: nil) ?? UIView()
+            if !(touchedSubview is UILabel) {
+                showMenuContext()
+            }
+        }
+        guard let label = touchedSubview as? UILabel else { return }
+        let touchPointLabel = gestureRecognizer.location(in: label)
+
+        if let text = label.text, let range = getWordRange(at: touchPointLabel, in: label) {
+            let word = String(text[range])
+            if word.starts(with: "www.") || word.starts(with: "https://") || word.starts(with: "http://") {
+                if gestureRecognizer.state == .cancelled || gestureRecognizer.state == .ended{
+                    timerCheckLink?.invalidate()
+                    if !isLinkCopied {
+                        var stringURl = word
+                        if stringURl.starts(with: "www.") {
+                            stringURl = "https://" + stringURl.replacingOccurrences(of: "www.", with: "")
+                        }
+                        guard let url = URL(string: stringURl) else { return }
+                        UIApplication.shared.open(url)
+                        label.attributedText = removeHighlightedText(for: text, in: range, label: label)
+                    }
+                    isLinkCopied = false
+                } else if gestureRecognizer.state == .began {
+                    label.attributedText = highlightedText(for: text, in: range, label: label)
+                    timerCheckLink = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: {_ in
+                        self.isLinkCopied = true
+                        UIPasteboard.general.string = word
+                        self.showToast(message: "Link Copied".localized(), font: UIFont.systemFont(ofSize: 12, weight: .medium), controller: self)
+                        label.attributedText = self.removeHighlightedText(for: text, in: range, label: label)
+                    })
+                }
+            } else {
+                showMenuContext()
+            }
+        } else {
+            showMenuContext()
+        }
+    }
+    
+    func getWordRange(at point: CGPoint, in label: UILabel) -> Range<String.Index>? {
+        guard let text = label.text else { return nil }
+        
+        let layoutManager = NSLayoutManager()
+        let textContainer = NSTextContainer(size: label.frame.size)
+        let textStorage = NSTextStorage(attributedString: NSAttributedString(string: text))
+        
+        layoutManager.addTextContainer(textContainer)
+        textStorage.addLayoutManager(layoutManager)
+        textContainer.lineFragmentPadding = 0
+        textContainer.maximumNumberOfLines = label.numberOfLines
+        
+        lastTouchPoint = point
+        let characterIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
+        
+        if characterIndex == text.count - 1 {
+            return nil
+        }
+        var wordStartIndex = characterIndex
+        while wordStartIndex > 0 && text[text.index(text.startIndex, offsetBy: wordStartIndex - 1)] != " " && text[text.index(text.startIndex, offsetBy: wordStartIndex - 1)] != "\n" {
+            wordStartIndex -= 1
+        }
+        
+        var wordEndIndex = characterIndex
+        while wordEndIndex < text.count && text[text.index(text.startIndex, offsetBy: wordEndIndex)] != " " && text[text.index(text.startIndex, offsetBy: wordEndIndex)] != "\n" {
+            wordEndIndex += 1
+        }
+        
+        return text.index(text.startIndex, offsetBy: wordStartIndex)..<text.index(text.startIndex, offsetBy: wordEndIndex)
+    }
+
+    func highlightedText(for text: String, in range: Range<String.Index>, label: UILabel) -> NSAttributedString {
+        let mutableAttributedString = label.attributedText!.mutableCopy() as! NSMutableAttributedString
+        mutableAttributedString.addAttribute(.backgroundColor, value: UIColor.lightGray.withAlphaComponent(0.5), range: NSRange(range, in: text))
+        return mutableAttributedString
+    }
+    
+    func removeHighlightedText(for text: String, in range: Range<String.Index>, label: UILabel) -> NSAttributedString {
+        let mutableAttributedString = label.attributedText!.mutableCopy() as! NSMutableAttributedString
+        mutableAttributedString.removeAttribute(.backgroundColor, range: NSRange(range, in: text))
+        return mutableAttributedString
+    }
+    
     @objc func tapMessageText(_ sender: ObjectGesture) {
         var stringURl = sender.message_id.lowercased()
         if stringURl.starts(with: "www.") {

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

@@ -945,10 +945,17 @@ extension ContactChatViewController {
                 if indexPath.row == 0 {
                     group = fillteredData[indexPath.section] as! Group
                 } else {
-                    group = (fillteredData[indexPath.section] as! Group).childs[indexPath.row - 1]
+                    if (fillteredData[indexPath.section] as! Group).childs.count > 0 {
+                        group = (fillteredData[indexPath.section] as! Group).childs[indexPath.row - 1]
+                    } else {
+                        return cell
+                    }
                 }
             } else {
                 if indexPath.row == 0 {
+                    if indexPath.section > (groups.count - 1) {
+                        return cell
+                    }
                     group = groups[indexPath.section]
                 } else {
                     group = groups[indexPath.section].childs[indexPath.row - 1]