Ver Fonte

update fix bugs for 5.0.31

alqindiirsyam há 3 meses atrás
pai
commit
d491b1af0c

+ 12 - 10
AppBuilder/AppBuilder/SecondTabViewController.swift

@@ -1126,19 +1126,21 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
         }
         data.isSelected = !data.isSelected
         if data.isSelected {
-            for dataSubChat in self.chatGroupMaps[data.groupId]! {
-                if var indexParent = chats.firstIndex(where: { $0.isParent && $0.groupId == data.groupId }) {
-                    if isFiltering || selectedTag == UNREAD_TAG {
-                        if var indexParentFilter = fillteredData.firstIndex(where: { ($0 as! Chat).isParent && ($0 as! Chat).groupId == data.groupId }) {
-                            fillteredData.insert(dataSubChat, at: indexParentFilter + 1)
-                            indexParentFilter+=1
+            if let dataSubChats = self.chatGroupMaps[data.groupId] {
+                for dataSubChat in dataSubChats {
+                    if var indexParent = chats.firstIndex(where: { $0.isParent && $0.groupId == data.groupId }) {
+                        if isFiltering || selectedTag == UNREAD_TAG {
+                            if var indexParentFilter = fillteredData.firstIndex(where: { ($0 as! Chat).isParent && ($0 as! Chat).groupId == data.groupId }) {
+                                fillteredData.insert(dataSubChat, at: indexParentFilter + 1)
+                                indexParentFilter+=1
+                            }
+                        } else {
+                            chats.insert(dataSubChat, at: indexParent + 1)
+                            indexParent+=1
                         }
-                    } else {
-                        chats.insert(dataSubChat, at: indexParent + 1)
-                        indexParent+=1
                     }
+                    
                 }
-                
             }
         } else {
             if isFiltering || selectedTag == UNREAD_TAG {

+ 12 - 8
NexilisLite/NexilisLite.xcodeproj/project.pbxproj

@@ -62,13 +62,11 @@
 		CD1E722D2A0BA86100BF871F /* QmeraAudioConference.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E717E2A0BA86100BF871F /* QmeraAudioConference.swift */; };
 		CD1E722E2A0BA86100BF871F /* AudioViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E717F2A0BA86100BF871F /* AudioViewController.swift */; };
 		CD1E722F2A0BA86100BF871F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E71802A0BA86100BF871F /* ProfileView.swift */; };
-		CD1E72302A0BA86100BF871F /* CallProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E71812A0BA86100BF871F /* CallProviderDelegate.swift */; };
 		CD1E72312A0BA86100BF871F /* QmeraAudioViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E71822A0BA86100BF871F /* QmeraAudioViewController.swift */; };
 		CD1E72322A0BA86100BF871F /* QmeraCallContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E71832A0BA86100BF871F /* QmeraCallContactViewController.swift */; };
 		CD1E72332A0BA86100BF871F /* WhiteboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E71842A0BA86100BF871F /* WhiteboardViewController.swift */; };
 		CD1E72342A0BA86100BF871F /* QmeraVideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E71852A0BA86100BF871F /* QmeraVideoViewController.swift */; };
 		CD1E72352A0BA86100BF871F /* VideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E71862A0BA86100BF871F /* VideoViewController.swift */; };
-		CD1E72362A0BA86100BF871F /* Call.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E71872A0BA86100BF871F /* Call.swift */; };
 		CD1E72372A0BA86100BF871F /* GroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E71882A0BA86100BF871F /* GroupView.swift */; };
 		CD1E72382A0BA86100BF871F /* GroupCreateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E718A2A0BA86100BF871F /* GroupCreateViewController.swift */; };
 		CD1E72392A0BA86100BF871F /* BroadcastMembersTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1E718B2A0BA86100BF871F /* BroadcastMembersTableViewCell.swift */; };
@@ -344,13 +342,11 @@
 		CD1E717E2A0BA86100BF871F /* QmeraAudioConference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QmeraAudioConference.swift; sourceTree = "<group>"; };
 		CD1E717F2A0BA86100BF871F /* AudioViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioViewController.swift; sourceTree = "<group>"; };
 		CD1E71802A0BA86100BF871F /* ProfileView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
-		CD1E71812A0BA86100BF871F /* CallProviderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallProviderDelegate.swift; sourceTree = "<group>"; };
 		CD1E71822A0BA86100BF871F /* QmeraAudioViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QmeraAudioViewController.swift; sourceTree = "<group>"; };
 		CD1E71832A0BA86100BF871F /* QmeraCallContactViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QmeraCallContactViewController.swift; sourceTree = "<group>"; };
 		CD1E71842A0BA86100BF871F /* WhiteboardViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhiteboardViewController.swift; sourceTree = "<group>"; };
 		CD1E71852A0BA86100BF871F /* QmeraVideoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QmeraVideoViewController.swift; sourceTree = "<group>"; };
 		CD1E71862A0BA86100BF871F /* VideoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoViewController.swift; sourceTree = "<group>"; };
-		CD1E71872A0BA86100BF871F /* Call.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Call.swift; sourceTree = "<group>"; };
 		CD1E71882A0BA86100BF871F /* GroupView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupView.swift; sourceTree = "<group>"; };
 		CD1E718A2A0BA86100BF871F /* GroupCreateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupCreateViewController.swift; sourceTree = "<group>"; };
 		CD1E718B2A0BA86100BF871F /* BroadcastMembersTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BroadcastMembersTableViewCell.swift; sourceTree = "<group>"; };
@@ -766,10 +762,8 @@
 			isa = PBXGroup;
 			children = (
 				CD1E717F2A0BA86100BF871F /* AudioViewController.swift */,
-				CD1E71872A0BA86100BF871F /* Call.swift */,
 				CD6312582DA925260088964E /* CallLogVC.swift */,
 				CD1E717D2A0BA86100BF871F /* CallManager.swift */,
-				CD1E71812A0BA86100BF871F /* CallProviderDelegate.swift */,
 				CD43C80A2D687DBF00297C6E /* CreateConferenceCallController.swift */,
 				CD1E71882A0BA86100BF871F /* GroupView.swift */,
 				CD1E71802A0BA86100BF871F /* ProfileView.swift */,
@@ -1266,10 +1260,14 @@
 			inputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-NexilisLite/Pods-NexilisLite-resources-${CONFIGURATION}-input-files.xcfilelist",
 			);
+			inputPaths = (
+			);
 			name = "[CP] Copy Pods Resources";
 			outputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-NexilisLite/Pods-NexilisLite-resources-${CONFIGURATION}-output-files.xcfilelist",
 			);
+			outputPaths = (
+			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NexilisLite/Pods-NexilisLite-resources.sh\"\n";
@@ -1283,10 +1281,14 @@
 			inputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-NexilisLite-NexilisLiteTests/Pods-NexilisLite-NexilisLiteTests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
 			);
+			inputPaths = (
+			);
 			name = "[CP] Embed Pods Frameworks";
 			outputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-NexilisLite-NexilisLiteTests/Pods-NexilisLite-NexilisLiteTests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
 			);
+			outputPaths = (
+			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NexilisLite-NexilisLiteTests/Pods-NexilisLite-NexilisLiteTests-frameworks.sh\"\n";
@@ -1300,10 +1302,14 @@
 			inputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-NexilisLite-NexilisLiteTests/Pods-NexilisLite-NexilisLiteTests-resources-${CONFIGURATION}-input-files.xcfilelist",
 			);
+			inputPaths = (
+			);
 			name = "[CP] Copy Pods Resources";
 			outputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-NexilisLite-NexilisLiteTests/Pods-NexilisLite-NexilisLiteTests-resources-${CONFIGURATION}-output-files.xcfilelist",
 			);
+			outputPaths = (
+			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NexilisLite-NexilisLiteTests/Pods-NexilisLite-NexilisLiteTests-resources.sh\"\n";
@@ -1395,7 +1401,6 @@
 				1241AEBD2D017E8C0088175A /* FileEncryption.swift in Sources */,
 				CD1E725A2A0BA86100BF871F /* ScannerViewController.swift in Sources */,
 				CD1E72462A0BA86100BF871F /* GroupTopicViewController.swift in Sources */,
-				CD1E72302A0BA86100BF871F /* CallProviderDelegate.swift in Sources */,
 				CD1E72472A0BA86100BF871F /* SettingTableViewController.swift in Sources */,
 				CD1E72172A0BA86100BF871F /* CategoryCC.swift in Sources */,
 				CD1E72552A0BA86100BF871F /* BroadcastMembersTableViewController.swift in Sources */,
@@ -1459,7 +1464,6 @@
 				CD1E721E2A0BA86100BF871F /* BNIBookingWebView.swift in Sources */,
 				CD1E72482A0BA86100BF871F /* BroadcastModeViewController.swift in Sources */,
 				CD1E72492A0BA86100BF871F /* BackupRestoreOption.swift in Sources */,
-				CD1E72362A0BA86100BF871F /* Call.swift in Sources */,
 				CD1E723D2A0BA86100BF871F /* CheckConnection.swift in Sources */,
 				CD1E72262A0BA86100BF871F /* QmeraGroupChooserViewController.swift in Sources */,
 				CD1E72372A0BA86100BF871F /* GroupView.swift in Sources */,

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

@@ -7,6 +7,7 @@
 //
 
 import Foundation
+import UIKit
 
 public class CoreMessage_TMessageBank {
     

+ 13 - 1
NexilisLite/NexilisLite/Source/Extension.swift

@@ -728,6 +728,19 @@ extension UITextView {
         }
 
     }
+    
+    func rangesOfMentionText(withColor color: UIColor) -> [NSRange] {
+        var ranges: [NSRange] = []
+        guard let attributedText = self.attributedText else { return ranges }
+        
+        attributedText.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: attributedText.length), options: []) { value, range, _ in
+            if let foundColor = value as? UIColor, foundColor == color {
+                ranges.append(range)
+            }
+        }
+        
+        return ranges
+    }
 
 }
 
@@ -857,7 +870,6 @@ extension String {
             if let member = Member.getMember(f_pin: username) {
                 let fullName = "\(member.firstName) \(member.lastName)".trimmingCharacters(in: .whitespaces)
                 text.replaceCharacters(in: range, with: fullName)
-                
                 if !groupID.isEmpty, Member.getMemberInGroup(f_pin: username, group_id: groupID) != nil {
                     let newRange = (text.string as NSString).range(of: "@\(fullName)")
                     text.addAttribute(.foregroundColor, value: UIColor.mentionColor, range: newRange)

+ 13 - 5
NexilisLite/NexilisLite/Source/View/Call/QmeraAudioViewController.swift

@@ -267,7 +267,7 @@ class QmeraAudioViewController: UIViewController {
     
     static func turnSpeakerOn() {
 //        var bAudioEngineIsAvtive: Bool! = false
-        API.turnSpeakerPhone(bSPon: bSpeakerPhone)
+//        API.turnSpeakerPhone(bSPon: bSpeakerPhone)
 //        repeat {
 //            Thread.sleep(forTimeInterval : 0.3)
 //            bAudioEngineIsAvtive = API.bAudioEngineIsRunning()
@@ -277,19 +277,25 @@ class QmeraAudioViewController: UIViewController {
 //            }
 //            API.restartAudioEngine()
 //        } while (!bAudioEngineIsAvtive)
-        var volume:Float! = 0
+//        var volume:Float! = 0
+        do {
+            let audioSession = AVAudioSession.sharedInstance()
+            try audioSession.overrideOutputAudioPort(bSpeakerPhone ? .speaker : .none)
+        } catch {
+            
+        }
         if (bSpeakerPhone) {
             DispatchQueue.main.async {
                 UIDevice.current.isProximityMonitoringEnabled = false
             }
-            volume = lastVolume * nMaxSPOn
+//            volume = lastVolume * nMaxSPOn
         } else {
             DispatchQueue.main.async {
                 UIDevice.current.isProximityMonitoringEnabled = true
             }
-            volume = lastVolume * nMaxSPOff
+//            volume = lastVolume * nMaxSPOff
         }
-        API.adjustVolume(fValue: volume)
+//        API.adjustVolume(fValue: volume)
     }
     
 //    static func toggleSpeakerPhone() {
@@ -414,6 +420,7 @@ class QmeraAudioViewController: UIViewController {
                 }
             } else if !ticketId.isEmpty {
                 if isOutgoing {
+                    self.backToDefaultAudioSession()
                     API.ccs(sTicketID: ticketId, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: listRemoteViewFix, ivLocalView: cameraView, ivRemoteZ: zoomView, bCameraOn: false)
                     if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getIncomingCallCS(f_pin_opposite: u.pin), timeout: 30 * 1000){
                         if response.mBodies[CoreMessage_TMessageKey.ERRCOD] != "01" {
@@ -421,6 +428,7 @@ class QmeraAudioViewController: UIViewController {
                         }
                     }
                 } else {
+                    self.backToDefaultAudioSession()
                     API.csa(sTicketID: ticketId, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: listRemoteViewFix, ivLocalView: cameraView, ivRemoteZ: zoomView, bCameraOn: false)
                 }
             } else if autoAcceptAPN {

+ 23 - 20
NexilisLite/NexilisLite/Source/View/Call/QmeraVideoViewController.swift

@@ -146,8 +146,14 @@ class QmeraVideoViewController: UIViewController {
     }()
     
     static func turnSpeakerOn() {
+        do {
+            let audioSession = AVAudioSession.sharedInstance()
+            try audioSession.overrideOutputAudioPort(bSpeakerPhone ? .speaker : .none)
+        } catch {
+            
+        }
 //        var bAudioEngineIsAvtive: Bool! = false
-        API.turnSpeakerPhone(bSPon: bSpeakerPhone)
+//        API.turnSpeakerPhone(bSPon: bSpeakerPhone)
 //        repeat {
 //            Thread.sleep(forTimeInterval : 0.3)
 //            bAudioEngineIsAvtive = API.bAudioEngineIsRunning()
@@ -157,13 +163,13 @@ class QmeraVideoViewController: UIViewController {
 //            }
 //            API.restartAudioEngine()
 //        } while (!bAudioEngineIsAvtive)
-        var volume:Float! = 0
-        if (bSpeakerPhone) {
-            volume = lastVolume * nMaxSPOn
-        } else {
-            volume = lastVolume * nMaxSPOff
-        }
-        API.adjustVolume(fValue: volume)
+//        var volume:Float! = 0
+//        if (bSpeakerPhone) {
+//            volume = lastVolume * nMaxSPOn
+//        } else {
+//            volume = lastVolume * nMaxSPOff
+//        }
+//        API.adjustVolume(fValue: volume)
     }
 
 //    static func toggleSpeakerPhone() {
@@ -524,6 +530,7 @@ class QmeraVideoViewController: UIViewController {
                     API.initiateCCall(sParty: dataPerson[0]["f_pin"]!, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: listRemoteViewFix, ivLocalView: cameraView, ivRemoteZ: zoomView)
                 }
             } else {
+                backToDefaultAudioSession()
                 API.ccs(sTicketID: ticketId, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: listRemoteViewFix, ivLocalView: cameraView, ivRemoteZ: zoomView, bCameraOn: true)
                 if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getIncomingCallCS(f_pin_opposite: users[0].pin), timeout: 30 * 1000){
                     if response.mBodies[CoreMessage_TMessageKey.ERRCOD] != "01" {
@@ -756,6 +763,7 @@ class QmeraVideoViewController: UIViewController {
             if ticketId.isEmpty {
                 API.receiveCCall(sParty: dataPerson[0]["f_pin"]!, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: listRemoteViewFix, ivLocalView: cameraView,ivRemoteZ: zoomView)
             } else {
+                backToDefaultAudioSession()
                 API.csa(sTicketID: ticketId, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: listRemoteViewFix, ivLocalView: cameraView, ivRemoteZ: zoomView, bCameraOn: true)
             }
         }
@@ -1279,21 +1287,16 @@ class QmeraVideoViewController: UIViewController {
             }
         } else if (state == Nexilis.VIDEO_CALL_ZOOM) && self.dataPerson.count > 1 {
             DispatchQueue.main.async {
-                if arrayMessage[0] == arrayMessage[3] {
-                    self.zoomView.transform   = CGAffineTransform.init(scaleX: -1.9, y: 2.2).rotated(by: (-CGFloat.pi)/2)
-                    self.zoomView.contentMode = .scaleAspectFit
-                } else {
-                    self.zoomView.transform   = CGAffineTransform.init(scaleX: -1.9, y: 2.2).rotated(by: (CGFloat.pi)/2)
-                    self.zoomView.contentMode = .scaleAspectFit
-                }
+                self.zoomView.transform   = CGAffineTransform.init(scaleX: 1.9, y: 2.0).rotated(by: (-CGFloat.pi)/2)
+                self.zoomView.contentMode = .scaleAspectFit
             }
         } else if (state == Nexilis.VIDEO_CAMERA_PARAMS_CHANED){
             if(arrayMessage[3] == "0"){
                 DispatchQueue.main.async {
                     if self.dataPerson.count == 1 && arrayMessage[2] == "1" && arrayMessage[4] == "1" {
-                        self.zoomView.transform = CGAffineTransform.init(scaleX: 1.9, y: 2.2).rotated(by: (-CGFloat.pi)/2)
+                        self.zoomView.transform = CGAffineTransform.init(scaleX: 1.9, y: 2.0).rotated(by: (-CGFloat.pi)/2)
                     } else {
-                        self.zoomView.transform = CGAffineTransform.init(scaleX: 1.9, y: 2.2).rotated(by: (CGFloat.pi)/2)
+                        self.zoomView.transform = CGAffineTransform.init(scaleX: 1.9, y: 2.0).rotated(by: (CGFloat.pi)/2)
                     }
                 }
             }
@@ -1424,13 +1427,13 @@ class QmeraVideoViewController: UIViewController {
                 }
                 if arrayMessage[5] == "2" && self.dataPerson.count == 1 {
                     DispatchQueue.main.async {
-                        self.zoomView.transform   = CGAffineTransform.init(scaleX: -1.9, y: 2.2).rotated(by: (CGFloat.pi)/2)
+                        self.zoomView.transform   = CGAffineTransform.init(scaleX: -1.9, y: 2.0).rotated(by: (CGFloat.pi)/2)
                         self.zoomView.contentMode = .scaleAspectFit
                     }
                 }
                 else if self.dataPerson.count == 1 {
                     DispatchQueue.main.async {
-                        self.zoomView.transform   = CGAffineTransform.init(scaleX: 1.9, y: 2.2).rotated(by: (-CGFloat.pi)/2)
+                        self.zoomView.transform   = CGAffineTransform.init(scaleX: 1.9, y: 2.0).rotated(by: (-CGFloat.pi)/2)
                         self.zoomView.contentMode = .scaleAspectFit
                     }
                 } else if self.dataPerson.count > 1 {
@@ -1618,7 +1621,7 @@ class QmeraVideoViewController: UIViewController {
                     
                     if self.dataPerson.count == 1 {
                         self.transformZoomAfterNewUserMore2 = false
-                        self.zoomView.transform   = CGAffineTransform.init(scaleX: 1.9, y: 2.2).rotated(by: (CGFloat.pi)/2)
+                        self.zoomView.transform   = CGAffineTransform.init(scaleX: 1.9, y: 2.0).rotated(by: (CGFloat.pi)/2)
                         
                         if !self.users[0].isConnected {
                             self.resetViewToOutgoing()

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

@@ -1801,18 +1801,18 @@ extension ChatGPTBotView: UITableViewDelegate, UITableViewDataSource {
         cell.contentView.addSubview(timeMessage)
         timeMessage.translatesAutoresizingMaskIntoConstraints = false
         if (dataMessages[indexPath.row]["read_receipts"] as? String) == "8" || ((dataMessages[indexPath.row]["credential"] as? String) == "1" && dataMessages[indexPath.row]["lock"] as? String != "2") {
-            timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40 - 20).isActive = true
+            timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40).isActive = true
         } else {
-            timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5 - 20).isActive = true
+            timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5).isActive = true
         }
         
         let statusMessage = UIImageView()
         if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
             containerMessage.leadingAnchor.constraint(greaterThanOrEqualTo: cell.contentView.leadingAnchor, constant: 60).isActive = true
             if (dataMessages[indexPath.row]["read_receipts"] as? String) == "8" || ((dataMessages[indexPath.row]["credential"] as? String) == "1" && dataMessages[indexPath.row]["lock"] as? String != "2") {
-                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40).isActive = true
             } else {
-                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5).isActive = true
             }
             containerMessage.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 5).isActive = true
             containerMessage.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -15).isActive = true
@@ -1886,9 +1886,9 @@ extension ChatGPTBotView: UITableViewDelegate, UITableViewDataSource {
                 containerMessage.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 15).isActive = true
             }
             if (dataMessages[indexPath.row]["read_receipts"] as? String) == "8" || ((dataMessages[indexPath.row]["credential"] as? String) == "1" && dataMessages[indexPath.row]["lock"] as? String != "2") {
-                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40).isActive = true
             } else {
-                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5).isActive = true
             }
             containerMessage.trailingAnchor.constraint(lessThanOrEqualTo: cell.contentView.trailingAnchor, constant: -60).isActive = true
             containerMessage.widthAnchor.constraint(greaterThanOrEqualToConstant: 46).isActive = true

+ 16 - 14
NexilisLite/NexilisLite/Source/View/Chat/ChatWALikeVC.swift

@@ -1085,23 +1085,25 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
         }
         data.isSelected = !data.isSelected
         if data.isSelected {
-            for dataSubChat in self.chatGroupMaps[data.groupId]! {
-                if var indexParent = chats.firstIndex(where: { $0.isParent && $0.groupId == data.groupId }) {
-                    if isFiltering || selectedTag == UNREAD_TAG {
-                        if var indexParentFilter = fillteredData.firstIndex(where: { ($0 as! Chat).isParent && ($0 as! Chat).groupId == data.groupId }) {
-                            fillteredData.insert(dataSubChat, at: indexParentFilter + 1)
-                            indexParentFilter+=1
-                        }
-                    } else {
-                        chats.insert(dataSubChat, at: indexParent + 1)
-                        indexParent+=1
-                        if var indexParentFilter = tempChats.firstIndex(where: { $0.isParent && $0.groupId == data.groupId }) {
-                            tempChats.insert(dataSubChat, at: indexParentFilter + 1)
-                            indexParentFilter+=1
+            if let dataSubChats = self.chatGroupMaps[data.groupId] {
+                for dataSubChat in dataSubChats {
+                    if var indexParent = chats.firstIndex(where: { $0.isParent && $0.groupId == data.groupId }) {
+                        if isFiltering || selectedTag == UNREAD_TAG {
+                            if var indexParentFilter = fillteredData.firstIndex(where: { ($0 as! Chat).isParent && ($0 as! Chat).groupId == data.groupId }) {
+                                fillteredData.insert(dataSubChat, at: indexParentFilter + 1)
+                                indexParentFilter+=1
+                            }
+                        } else {
+                            chats.insert(dataSubChat, at: indexParent + 1)
+                            indexParent+=1
+                            if var indexParentFilter = tempChats.firstIndex(where: { $0.isParent && $0.groupId == data.groupId }) {
+                                tempChats.insert(dataSubChat, at: indexParentFilter + 1)
+                                indexParentFilter+=1
+                            }
                         }
                     }
+                    
                 }
-                
             }
         } else {
             if isFiltering || selectedTag == UNREAD_TAG {

+ 2 - 0
NexilisLite/NexilisLite/Source/View/Chat/CustomTextView.swift

@@ -83,6 +83,8 @@ class CustomTextView: UITextView {
                 customDelegate?.customTextViewDidPasteText(image: pasteboardItems["public.png"] as? UIImage ?? pasteboardItems["public.jpeg"] as? UIImage, dataGIF: dataGif)
                 return
             }
+        } else if let string = UIPasteboard.general.string {
+            self.replace(self.selectedTextRange!, withText: string)
         }
         super.paste(sender)
     }

+ 282 - 154
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -87,6 +87,8 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     var keyboardHeightForMention: CGFloat?
     var listMentionWithText:[User] = []
     var listMentionInTextField:[User] = []
+    var tempListMentionWithText:[User] = []
+    var tempListMentionInTextField:[User] = []
     var showingLink = ""
     var isAlwaysHideLinkPreview = false
     var timerCheckLink: Timer?
@@ -119,6 +121,8 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     var playingIndexPath: IndexPath?
     var timerSearch: Timer?
     
+    var tableMentionEdit = UITableView()
+    
     func offset() -> CGFloat{
         guard let fontSize = Int(SecureUserDefaults.shared.value(forKey: "font_size") ?? "0") else { return 0 }
         return CGFloat(fontSize)
@@ -1517,8 +1521,11 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     }
     
     @IBAction func stickerTapped(_ sender: UIButton) {
+        if textFieldSend.isFirstResponder {
+            dismissKeyboard()
+        }
         DispatchQueue.main.async {
-            if (self.constraintBottomAttachment.constant == 0.0) {
+            if !self.viewSticker.isDescendant(of: self.view) {
                 self.constraintBottomAttachment.constant = 200.0
                 self.view.addSubview(self.viewSticker)
                 self.viewSticker.translatesAutoresizingMaskIntoConstraints = false
@@ -1671,10 +1678,11 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
             let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
             
             if self.constraintViewTextField.constant != keyboardHeight - 60 {
-                if self.contraintBottomMention.constant > 0 && self.contraintBottomMention.constant < self.contraintBottomMention.constant + keyboardHeight - 60 {
-                    self.contraintBottomMention.constant = self.contraintBottomMention.constant + keyboardHeight - 60
+//                self.constraintViewTextField.constant = keyboardHeight - 60
+                self.constraintBottomAttachment.constant = keyboardHeight
+                if self.contraintBottomMention.constant > 0 {
+                    self.contraintBottomMention.constant = 25 + constraintBottomAttachment.constant + self.heightTextFieldSend.constant + self.viewTextfield.bounds.height
                 }
-                self.constraintViewTextField.constant = keyboardHeight - 60
                 self.keyboardHeightForMention = keyboardHeight
                 if isSearching {
                     self.constraintViewTextField.constant = self.constraintViewTextField.constant + 60
@@ -1710,10 +1718,11 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
             let info:NSDictionary = notification.userInfo! as NSDictionary
             let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
             
-            self.constraintViewTextField.constant = 0
+//            self.constraintViewTextField.constant = 0
+            self.constraintBottomAttachment.constant = 0
             self.constraintBottomContainerMultpileSelectSession.constant = 0
-            if self.contraintBottomMention.constant > 0 && self.keyboardHeightForMention != nil {
-                self.contraintBottomMention.constant = self.contraintBottomMention.constant - self.keyboardHeightForMention! + 60
+            if self.contraintBottomMention.constant > 0 {
+                self.contraintBottomMention.constant = 25 + constraintBottomAttachment.constant + self.heightTextFieldSend.constant + self.viewTextfield.bounds.height
             }
             keyboardHeightForMention = nil
             UIView.animate(withDuration: TimeInterval(duration), animations: {
@@ -1723,7 +1732,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     }
     
     @objc func showChooserACKConfidential() {
-        dismissKeyboard()
+//        dismissKeyboard()
         let alertController = LibAlertController(title: "Message Mode".localized(), message: "Select".localized() + " " + "Message Mode".localized(), preferredStyle: .actionSheet)
         let imageConfidential = resizeImage(image: UIImage(named: "pb_icon_conf_msg_on", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal)
         let imageAck = resizeImage(image: UIImage(named: "pb_icon_ack_msg_on", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal)
@@ -1758,7 +1767,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         stickerAction.setValue(imageSticker, forKey: "image")
         alertController.addAction(confidentialAction)
         alertController.addAction(ackAction)
-        alertController.addAction(stickerAction)
+//        alertController.addAction(stickerAction)
         alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: { (UIAlertAction) in
             self.isConfidential = false
             self.isAck = false
@@ -2507,92 +2516,94 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
     
     public func textViewDidChangeSelection(_ textView: UITextView) {
         lastPositionCursorMention = textView.selectedRange.location
-        guard lastPositionCursorMention > 0 else {
-            hideMention()
-            return
-        }
 
-        let fulltextForMention = textView.text.prefix(lastPositionCursorMention)
-        var isShowMention = false
-        var listHaveToRemoved: [User] = []
-        var continueCheckMention = true
+        if lastPositionCursorMention > 0 {
+            let fulltextForMention = textView.text.prefix(lastPositionCursorMention)
+            var isShowMention = false
+            var listHaveToRemoved: [User] = []
+            var continueCheckMention = true
 
-        for mention in listMentionInTextField where mention.ex_block?.isEmpty == false {
-            let nameWithMention = "@\(mention.firstName) \(mention.lastName)".trimmingCharacters(in: .whitespaces)
+            for mention in listMentionInTextField where mention.ex_block?.isEmpty == false {
+                let nameWithMention = "@\(mention.firstName) \(mention.lastName)".trimmingCharacters(in: .whitespaces)
 
-            guard let blockPosition = Int(mention.ex_block ?? ""), blockPosition >= 0 else { continue }
+                guard let blockPosition = Int(mention.ex_block ?? ""), blockPosition >= 0 else { continue }
 
-            guard !textView.text.isEmpty else { continue }
+                guard !textView.text.isEmpty else { continue }
 
-            let textCount = textView.text.count
-            let lowerOffset = max(0, blockPosition - nameWithMention.count)
-            let upperOffset = min(textCount, blockPosition)
+                let textCount = textView.text.count
+                let lowerOffset = max(0, blockPosition - nameWithMention.count)
+                let upperOffset = min(textCount, blockPosition)
 
-            guard lowerOffset < textCount, upperOffset <= textCount else { continue }
+                guard lowerOffset < textCount, upperOffset <= textCount else { continue }
 
-            let lowerBound = textView.text.index(textView.text.startIndex, offsetBy: lowerOffset)
-            let upperBound = textView.text.index(textView.text.startIndex, offsetBy: upperOffset)
-            let range = lowerBound..<upperBound
-            
-            if textView.text[range] == nameWithMention {
-                if lastPositionCursorMention >= textView.text.distance(from: textView.text.startIndex, to: lowerBound) + 1,
-                   lastPositionCursorMention <= textView.text.distance(from: textView.text.startIndex, to: upperBound) {
-                    continueCheckMention = false
-                    break
-                }
-            } else {
-                let exOffmpValue = Int(mention.ex_offmp ?? "") ?? 0
+                let lowerBound = textView.text.index(textView.text.startIndex, offsetBy: lowerOffset)
+                let upperBound = textView.text.index(textView.text.startIndex, offsetBy: upperOffset)
+                let range = lowerBound..<upperBound
+                
+                if textView.text[range] == nameWithMention {
+                    if lastPositionCursorMention >= textView.text.distance(from: textView.text.startIndex, to: lowerBound) + 1,
+                       lastPositionCursorMention <= textView.text.distance(from: textView.text.startIndex, to: upperBound) {
+                        continueCheckMention = false
+                        break
+                    }
+                } else {
+                    let exOffmpValue = Int(mention.ex_offmp ?? "") ?? 0
 
-                let offset = mention.ex_offmp?.isEmpty == false ? listMentionWithText.count - exOffmpValue : listMentionWithText.count
+                    let offset = mention.ex_offmp?.isEmpty == false ? listMentionWithText.count - exOffmpValue : listMentionWithText.count
 
-                let safeOffset = max(-textView.text.distance(from: lowerBound, to: textView.text.endIndex),
-                                     min(offset, textView.text.distance(from: upperBound, to: textView.text.endIndex)))
+                    let safeOffset = max(-textView.text.distance(from: lowerBound, to: textView.text.endIndex),
+                                         min(offset, textView.text.distance(from: upperBound, to: textView.text.endIndex)))
 
-                if let adjustedLowerBound = textView.text.index(lowerBound, offsetBy: safeOffset, limitedBy: textView.text.endIndex),
-                   let adjustedUpperBound = textView.text.index(upperBound, offsetBy: safeOffset, limitedBy: textView.text.endIndex) {
-                    
-                    let adjustedRange = adjustedLowerBound..<adjustedUpperBound
-                    
-                    if textView.text[adjustedRange] == nameWithMention {
-                        if lastPositionCursorMention >= textView.text.distance(from: textView.text.startIndex, to: adjustedLowerBound) + 1,
-                           lastPositionCursorMention <= textView.text.distance(from: textView.text.startIndex, to: adjustedUpperBound) {
-                            continueCheckMention = false
-                            break
+                    if let adjustedLowerBound = textView.text.index(lowerBound, offsetBy: safeOffset, limitedBy: textView.text.endIndex),
+                       let adjustedUpperBound = textView.text.index(upperBound, offsetBy: safeOffset, limitedBy: textView.text.endIndex) {
+                        let adjustedRange = adjustedLowerBound..<adjustedUpperBound
+                        
+                        if textView.text[adjustedRange] == nameWithMention {
+                            if lastPositionCursorMention >= textView.text.distance(from: textView.text.startIndex, to: adjustedLowerBound) + 1,
+                               lastPositionCursorMention <= textView.text.distance(from: textView.text.startIndex, to: adjustedUpperBound) {
+                                continueCheckMention = false
+                                break
+                            }
+                            mention.ex_block = "\(textView.text.distance(from: textView.text.startIndex, to: adjustedUpperBound))"
+                            mention.ex_offmp = "\(textView.text.count)"
+                        } else {
+                            listHaveToRemoved.append(mention)
                         }
-                        mention.ex_block = "\(textView.text.distance(from: textView.text.startIndex, to: adjustedUpperBound))"
-                        mention.ex_offmp = "\(textView.text.count)"
-                    } else {
-                        listHaveToRemoved.append(mention)
                     }
                 }
             }
-        }
-
-        if continueCheckMention {
-            if let indexLastBreak = fulltextForMention.split(separator: "\n").lastIndex(where: { $0.contains("@") }) {
-                let splitSpace = fulltextForMention.split(separator: "\n")[indexLastBreak].split(separator: " ")
-                if let indexLastMention = splitSpace.lastIndex(where: { $0.hasPrefix("@") }),
-                   let lastChar = fulltextForMention.last,
-                   lastChar != " " && lastChar != "\n" {
-                    showMention(text: String(splitSpace[indexLastMention].dropFirst()))
-                    isShowMention = true
-                }
-            } else if fulltextForMention.count >= 3 && self.textFieldSend.textColor != UIColor.lightGray {
-                if let indexLastBreak = fulltextForMention.split(separator: "\n").lastIndex(where: { $0.count >= 3 }) {
+            
+            if continueCheckMention {
+                if let indexLastBreak = fulltextForMention.split(separator: "\n").lastIndex(where: { $0.contains("@") }) {
                     let splitSpace = fulltextForMention.split(separator: "\n")[indexLastBreak].split(separator: " ")
-                    if let indexLastMention = splitSpace.lastIndex(of: splitSpace.last ?? ""),
+                    if let indexLastMention = splitSpace.lastIndex(where: { $0.hasPrefix("@") }),
                        let lastChar = fulltextForMention.last,
                        lastChar != " " && lastChar != "\n" {
-                        if String(splitSpace[indexLastMention]).count >= 3 {
-                            showMention(text: String(splitSpace[indexLastMention]))
+                        let textM = String(splitSpace[indexLastMention].dropFirst())
+                        if lastPositionCursorMention == textM.count + 1 {
+                            showMention(text: textM)
                             isShowMention = true
                         }
                     }
+                } else if fulltextForMention.count >= 3 && self.textFieldSend.textColor != UIColor.lightGray {
+                    if let indexLastBreak = fulltextForMention.split(separator: "\n").lastIndex(where: { $0.count >= 3 }) {
+                        let splitSpace = fulltextForMention.split(separator: "\n")[indexLastBreak].split(separator: " ")
+                        if let indexLastMention = splitSpace.lastIndex(of: splitSpace.last ?? ""),
+                           let lastChar = fulltextForMention.last,
+                           lastChar != " " && lastChar != "\n" {
+                            if String(splitSpace[indexLastMention]).count >= 3 {
+                                showMention(text: String(splitSpace[indexLastMention]))
+                                isShowMention = true
+                            }
+                        }
+                    }
                 }
             }
-        }
-
-        if !isShowMention {
+            
+            if !isShowMention {
+                hideMention()
+            }
+        } else {
             hideMention()
         }
 
@@ -2603,10 +2614,20 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
         let cursorPosition = textView.caretRect(for: nowTextFieldSend!.selectedTextRange!.start).origin
         let doubleCurrentLine = cursorPosition.y / nowTextFieldSend!.font!.lineHeight
         if doubleCurrentLine.isFinite {
-            let currentLine = Int(doubleCurrentLine)
+            let currentLine = Int(ceil(doubleCurrentLine))
             UIView.animate(withDuration: 0.3) {
-                let numberOfLines = textView.textContainer.lineBreakMode == .byWordWrapping ? Int(textView.contentSize.height / textView.font!.lineHeight) - 1 : 1
-                if currentLine == 0 && numberOfLines == 1 {
+                let layoutManager = textView.layoutManager
+                var numberOfLines = 0
+                var index = 0
+                let numberOfGlyphs = layoutManager.numberOfGlyphs
+
+                while index < numberOfGlyphs {
+                    var lineRange = NSRange()
+                    layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
+                    index = NSMaxRange(lineRange)
+                    numberOfLines += 1
+                }
+                if currentLine == 1 && (numberOfLines == 1 || numberOfLines == 0) {
                     if self.isEditingMessage {
                         self.constraintHeighteditTextView.constant = 40
                     } else {
@@ -2711,7 +2732,6 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
                 let newCursorPosition = cursorPosition + 2  // Adjust cursor position
                 textView.text = replacedText
                 textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
-                return
             }
         }
 
@@ -2727,48 +2747,26 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
                 let newCursorPosition = cursorPosition + 2  // Adjust cursor
                 textView.text = replacedText
                 textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
-                return
             }
         }
 
-        // Handle Undo: If user removes the first letter, revert back to original "- " or "1. "
-        let bulletUndoPattern = #"(^|\n)  • $"# // Matches "  • " when the letter is removed
-        if let match = text.range(of: bulletUndoPattern, options: .regularExpression) {
-            let replacedText = text.replacingOccurrences(of: "  • ", with: "- ", range: match)
-            let newCursorPosition = cursorPosition - 2
-            
-            textView.text = replacedText
-            textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
-            return
-        }
-
-        let numberUndoPattern = #"(^|\n)  (\d+)\. $"# // Matches "  1. " when the letter is removed
-        if let match = text.range(of: numberUndoPattern, options: .regularExpression) {
-            let replacedText = text.replacingOccurrences(of: "  ", with: "", range: match)
-            let newCursorPosition = cursorPosition - 2
-            
-            textView.text = replacedText
-            textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
-        }
-        
-        textView.preserveCursorPosition(withChanges: { _ in
-            textView.attributedText = textView.text.richText(isEditing: true, group_id: self.dataGroup["group_id"]  as? String ?? "", listMentionInTextField: listMentionInTextField)
-            return .preserveCursor
-        })
+        handleRichText(textView)
     }
     
     private func showMention(text: String) {
         if self.contraintBottomMention.constant < 0 {
-            self.contraintBottomMention.constant = 25 + ((self.keyboardHeightForMention != nil) ? self.keyboardHeightForMention! : 0) + self.heightTextFieldSend.constant
-            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
+            if !isEditingMessage {
+                self.contraintBottomMention.constant = 25 + constraintBottomAttachment.constant + self.heightTextFieldSend.constant + self.viewTextfield.bounds.height
+                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()
+                })
             }
-            UIView.animate(withDuration: 0.5, animations: {
-                self.view.layoutIfNeeded()
-            })
         }
         listMentionWithText.removeAll()
         Database.shared.database?.inTransaction({ fmdb, rollback in
@@ -2791,16 +2789,18 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
                     cursor.close()
                 }
                 listMentionWithText.removeAll(where: { listMentionInTextField.contains($0) })
-                if listMentionWithText.count > 0 {
-                    if listMentionWithText.count < 5 {
-                        self.heightTableMention.constant = CGFloat(44 * listMentionWithText.count)
+                if !isEditingMessage {
+                    if listMentionWithText.count > 0 {
+                        if listMentionWithText.count < 5 {
+                            self.heightTableMention.constant = CGFloat(44 * listMentionWithText.count)
+                        } else {
+                            self.heightTableMention.constant = 44 * 4
+                        }
+                        tableMention.reloadData()
                     } else {
-                        self.heightTableMention.constant = 44 * 4
+                        self.heightTableMention.constant = 44
+                        self.hideMention()
                     }
-                    tableMention.reloadData()
-                } else {
-                    self.heightTableMention.constant = 44
-                    self.hideMention()
                 }
             } catch {
                 rollback.pointee = true
@@ -3029,8 +3029,8 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
             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
+                        let fulltextForMention = textView.text.substring(from: 0, to: lastPositionCursorMention - 1)
+                        let diff = textView.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)
@@ -3039,7 +3039,7 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
                         let copyAttributedText = text.richText(isEditing: true, group_id: self.dataGroup["group_id"]  as? String ?? "", listMentionInTextField: listMentionInTextField)
                         copyAttributedText.removeAttribute(.foregroundColor, range: rangeReplacement)
                         
-                        textFieldSend.attributedText = copyAttributedText
+                        textView.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),
@@ -3049,16 +3049,30 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
                         
                         listMentionInTextField.remove(at: i)
                         
-                        textFieldSend.attributedText = text.richText(isEditing: true, group_id: self.dataGroup["group_id"]  as? String ?? "", listMentionInTextField: listMentionInTextField)
+                        textView.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!)
+                        let newPosition = textView.position(from: textView.beginningOfDocument, offset: textView.text.count - diff)
+                        textView.selectedTextRange = textView.textRange(from: newPosition!, to: newPosition!)
+                        textViewDidChangeSelection(textView)
+                        handleRichText(textView)
                         return false
                     }
                 }
             }
         }
-        
+        let indent = handleIndent(textView, range, text)
+        if !indent {
+            textViewDidChangeSelection(textView)
+            handleRichText(textView)
+            return indent
+        }
+        if (textView.text.count == 0) {
+            return text != "\n"
+        }
+        return true
+    }
+    
+    private func handleIndent(_ textView: UITextView, _ range: NSRange, _ text: String) -> Bool {
         guard let nsText = textView.text as NSString? else { return true }
         let newText = nsText.replacingCharacters(in: range, with: text)
         var lines = newText.components(separatedBy: "\n")
@@ -3118,12 +3132,16 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
             textView.selectedRange = NSRange(location: range.location - 1, length: 0)
             return false
         }
-        if (self.textFieldSend.text.count == 0) {
-            return text != "\n"
-        }
         return true
     }
     
+    private func handleRichText(_ textView: UITextView) {
+        textView.preserveCursorPosition(withChanges: { _ in
+            textView.attributedText = textView.text.richText(isEditing: true, group_id: self.dataGroup["group_id"]  as? String ?? "")
+            return .preserveCursor
+        })
+    }
+    
     public func textView(_ textView: UITextView, shouldInteractWith URL: URL?, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
         var urlString: String?
 
@@ -3533,8 +3551,55 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
     }
     
     func showEditMessageView(at indexPath: IndexPath) {
+        tempListMentionWithText = listMentionWithText
+        tempListMentionInTextField = listMentionInTextField
+        listMentionWithText.removeAll()
+        listMentionInTextField.removeAll()
         let dataMessages = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[indexPath.section]})
         let oldText = dataMessages[indexPath.row][TypeDataMessage.message_text]  as? String ?? ""
+        var oldTextForTextview = oldText
+        let pattern = "@[\\w]+"
+        do {
+            let regex = try NSRegularExpression(pattern: pattern)
+            let nsrange = NSRange(oldText.startIndex..., in: oldText)
+            let matches = regex.matches(in: oldText, range: nsrange)
+            
+            let results = matches.map {
+                String(oldText[Range($0.range, in: oldText)!])
+            }
+            for result in results {
+                let pinRes = result.components(separatedBy: "@")[1]
+                Database.shared.database?.inTransaction({ fmdb, rollback in
+                    do {
+                        if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, first_name || ' ' || ifnull(last_name, '') name FROM GROUPZ_MEMBER where f_pin = '\(pinRes)'"), cursor.next() {
+                            let user = User(pin: "")
+                            user.pin = cursor.string(forColumnIndex: 0) ?? ""
+                            user.firstName = cursor.string(forColumnIndex: 1) ?? ""
+                            if !user.pin.isEmpty {
+                                var fixUser = User.getDataCanNil(pin: user.pin, fmdb: fmdb)
+                                if fixUser == nil {
+                                    fixUser = user
+                                }
+                                var indexAt = 0
+                                if let range = oldText.range(of: result) {
+                                    indexAt = oldText.distance(from: oldText.startIndex, to: range.lowerBound)
+                                }
+                                fixUser?.ex_block = "\(indexAt + fixUser!.fullName.count + 1)"
+                                listMentionWithText.append(fixUser!)
+                                listMentionInTextField.append(fixUser!)
+                                oldTextForTextview = oldTextForTextview.replacingOccurrences(of: result, with: "@\(fixUser!.fullName)")
+                            }
+                            cursor.close()
+                        }
+                    } catch {
+                        rollback.pointee = true
+                        print("Access database error: \(error.localizedDescription)")
+                    }
+                })
+            }
+        } catch {
+            print("Invalid regex pattern")
+        }
         editVC = UIViewController()
         if let view = editVC.view {
             view.backgroundColor = .clear
@@ -3548,8 +3613,8 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
             view.addSubview(blurView)
             blurView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor)
             
-            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissEditVC))
-            tapGesture.cancelsTouchesInView = false
+            let tapGesture = ObjectGesture(target: self, action: #selector(dismissEditVC))
+            tapGesture.message_id = oldTextForTextview
             view.addGestureRecognizer(tapGesture)
             
             editTextView = CustomTextView()
@@ -3569,15 +3634,51 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
             constraintHeighteditTextView = editTextView.heightAnchor.constraint(equalToConstant: 40)
             constraintBottomeditTextView.isActive = true
             constraintHeighteditTextView.isActive = true
-            editTextView.attributedText = oldText.richText(group_id: self.dataGroup["group_id"]  as? String ?? "")
+            editTextView.attributedText = oldTextForTextview.richText(isEditing: true, group_id: self.dataGroup["group_id"]  as? String ?? "")
             editTextView.becomeFirstResponder()
             
+//            tableMentionEdit = UITableView()
+//            view.addSubview(tableMentionEdit)
+//            tableMentionEdit.anchor(left: view.leftAnchor, right: view.rightAnchor, height: 44)
+            
+            
             buttonSendEdit.setImage(resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "Send-(White)", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(.blackDarkMode) : UIImage(named: "Send-(White)", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal), for: .normal)
             buttonSendEdit.circle()
             buttonSendEdit.isEnabled = true
             buttonSendEdit.actionHandle(controlEvents: .touchUpInside,
              ForAction:{() -> Void in
-                let newText = self.editTextView.text ?? ""
+                var newText = self.editTextView.text ?? ""
+                if newText.contains("@") && self.listMentionInTextField.count > 0 {
+                    var diff: Int = 0
+                    for i in 0..<self.listMentionInTextField.count {
+                        let mention = self.listMentionInTextField[i]
+                        guard let exBlockStr = mention.ex_block, let exBlock = Int(exBlockStr) else {
+                            continue // skip if ex_block is nil or not an integer
+                        }
+                        let nameWithMention = ("@" + mention.firstName + " " + mention.lastName).trimmingCharacters(in: .whitespaces)
+                        let pinString = "@\(mention.pin)"
+                        let upperBound = exBlock + diff - 1
+                        let lowerBound = upperBound - nameWithMention.count + 1
+                        guard lowerBound >= 0, upperBound < newText.count else {
+                            continue // prevent index out-of-range
+                        }
+                        var afterMention = ""
+                        let nextCharIndex = newText.index(newText.startIndex, offsetBy: upperBound + 1, limitedBy: newText.endIndex)
+                        if let index = nextCharIndex, index < newText.endIndex {
+                            let nextChar = newText[index]
+                            if nextChar != "\n" && nextChar != " " {
+                                afterMention = " "
+                            }
+                        }
+                        let startIndex = newText.index(newText.startIndex, offsetBy: lowerBound)
+                        let endIndex = newText.index(newText.startIndex, offsetBy: upperBound + 1)
+                        let range = startIndex..<endIndex
+                        if newText[range] == nameWithMention {
+                            newText.replaceSubrange(range, with: pinString + afterMention)
+                            diff += (pinString + afterMention).count - nameWithMention.count
+                        }
+                    }
+                }
                 if !newText.isEmpty && newText.trimmingCharacters(in: .whitespacesAndNewlines) != oldText {
                     let lastEdited = Int64(Date().currentTimeMillis())
                     let message = CoreMessage_TMessageBank.editMessage(message_id: dataMessages[indexPath.row][TypeDataMessage.message_id]  as? String ?? "", l_pin: dataMessages[indexPath.row][TypeDataMessage.l_pin]  as? String ?? "", message_scope_id: dataMessages[indexPath.row][TypeDataMessage.message_scope_id]  as? String ?? "", status: "1", message_text: newText, credential: dataMessages[indexPath.row][TypeDataMessage.credential]  as? String ?? "", attachment_flag: dataMessages[indexPath.row][TypeDataMessage.attachment_flag]  as? String ?? "", ex_blog_id: dataMessages[indexPath.row][TypeDataMessage.blog_id]  as? String ?? "", message_large_text: "", ex_format: "", image_id: dataMessages[indexPath.row][TypeDataMessage.image_id]  as? String ?? "", audio_id: dataMessages[indexPath.row][TypeDataMessage.audio_id]  as? String ?? "", video_id: dataMessages[indexPath.row][TypeDataMessage.video_id]  as? String ?? "", file_id: dataMessages[indexPath.row][TypeDataMessage.file_id]  as? String ?? "", thumb_id: dataMessages[indexPath.row][TypeDataMessage.thumb_id]  as? String ?? "", reff_id: dataMessages[indexPath.row][TypeDataMessage.reff_id]  as? String ?? "", read_receipts: dataMessages[indexPath.row][TypeDataMessage.read_receipts]  as? String ?? "", chat_id: dataMessages[indexPath.row][TypeDataMessage.chat_id]  as? String ?? "", is_call_center: dataMessages[indexPath.row][TypeDataMessage.is_call_center]  as? String ?? "", call_center_id: dataMessages[indexPath.row][TypeDataMessage.call_center_id]  as? String ?? "", opposite_pin: dataMessages[indexPath.row][TypeDataMessage.opposite_pin]  as? String ?? "", server_date: dataMessages[indexPath.row][TypeDataMessage.server_date]  as? String ?? "", local_time_stamp: dataMessages[indexPath.row][TypeDataMessage.server_date]  as? String ?? "", last_edit: lastEdited)
@@ -3603,6 +3704,10 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
                         self.tableChatView.reloadRows(at: [indexPath], with: .none)
                     }
                 }
+                self.isEditingMessage = false
+                self.listMentionWithText = self.tempListMentionWithText
+                self.listMentionInTextField = self.tempListMentionWithText
+                self.editVC.dismiss(animated: true)
              })
             buttonSendEdit.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .mainColor
             view.addSubview(buttonSendEdit)
@@ -3653,9 +3758,25 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
         })
     }
     
-    @objc func dismissEditVC() {
-        self.isEditingMessage = false
-        editVC.dismiss(animated: true)
+    @objc func dismissEditVC(_ sender: ObjectGesture) {
+        if editTextView.text == sender.message_id {
+            self.isEditingMessage = false
+            self.listMentionWithText = self.tempListMentionWithText
+            self.listMentionInTextField = self.tempListMentionWithText
+            editVC.dismiss(animated: true)
+        } else if self.isEditingMessage {
+            let alert = LibAlertController(title: "".localized(), message: "Discard edit?".localized(), preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: "Cancel".localized(), style: UIAlertAction.Style.cancel, handler: nil))
+            alert.addAction(UIAlertAction(title: "Discard".localized(), style: UIAlertAction.Style.default, handler: {(_) in
+                self.isEditingMessage = false
+                self.listMentionWithText = self.tempListMentionWithText
+                self.listMentionInTextField = self.tempListMentionWithText
+                self.editVC.dismiss(animated: true)
+            }))
+            editVC.present(alert, animated: true, completion: nil)
+        } else {
+            editVC.dismiss(animated: true)
+        }
     }
     
     @objc func cancelAction() {
@@ -4498,12 +4619,13 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
         containerMessage.translatesAutoresizingMaskIntoConstraints = false
         
         let timeMessage = UILabel()
+        timeMessage.numberOfLines = 0
         cellMessage.contentView.addSubview(timeMessage)
         timeMessage.translatesAutoresizingMaskIntoConstraints = false
         if (dataMessages[indexPath.row]["read_receipts"] as? String) == "8" || ((dataMessages[indexPath.row]["credential"] as? String) == "1" && dataMessages[indexPath.row]["lock"] as? String != "2") {
-            timeMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -40 - 20).isActive = true
+            timeMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -40).isActive = true
         } else {
-            timeMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -5 - 20).isActive = true
+            timeMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -5).isActive = true
         }
         
         let messageText = UITextView()
@@ -4594,9 +4716,9 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
             containerMessage.topAnchor.constraint(equalTo: cellMessage.contentView.topAnchor, constant: 5).isActive = true
             containerMessage.leadingAnchor.constraint(greaterThanOrEqualTo: cellMessage.contentView.leadingAnchor, constant: 60).isActive = true
             if (dataMessages[indexPath.row]["read_receipts"] as? String) == "8" || ((dataMessages[indexPath.row]["credential"] as? String) == "1" && dataMessages[indexPath.row]["lock"] as? String != "2") {
-                containerMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -40 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -40).isActive = true
             } else {
-                containerMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -5 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -5).isActive = true
             }
             containerMessage.trailingAnchor.constraint(equalTo: profileMessage.leadingAnchor, constant: -5).isActive = true
             containerMessage.widthAnchor.constraint(greaterThanOrEqualToConstant: 46).isActive = true
@@ -4815,20 +4937,20 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
             }
         }
         
-        if dataMessages[indexPath.row][TypeDataMessage.last_edit] != nil && dataMessages[indexPath.row][TypeDataMessage.last_edit] as! Int64 != 0 {
-            let editedText = UILabel()
-            editedText.text = "Edited".localized()
-            editedText.font = UIFont.systemFont(ofSize: 10 + offset(), weight: .medium)
-            editedText.textColor = .lightGray
-            cellMessage.contentView.addSubview(editedText)
-            editedText.translatesAutoresizingMaskIntoConstraints = false
-            if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
-                editedText.trailingAnchor.constraint(equalTo: timeMessage.leadingAnchor, constant: -2).isActive = true
-            } else {
-                editedText.leadingAnchor.constraint(equalTo: timeMessage.trailingAnchor, constant: 2).isActive = true
-            }
-            editedText.bottomAnchor.constraint(equalTo: containerMessage.bottomAnchor).isActive = true
-        }
+//        if dataMessages[indexPath.row][TypeDataMessage.last_edit] != nil && dataMessages[indexPath.row][TypeDataMessage.last_edit] as! Int64 != 0 {
+//            let editedText = UILabel()
+//            editedText.text = "Edited".localized()
+//            editedText.font = UIFont.systemFont(ofSize: 10 + offset(), weight: .medium)
+//            editedText.textColor = .lightGray
+//            cellMessage.contentView.addSubview(editedText)
+//            editedText.translatesAutoresizingMaskIntoConstraints = false
+//            if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+//                editedText.trailingAnchor.constraint(equalTo: timeMessage.leadingAnchor, constant: -2).isActive = true
+//            } else {
+//                editedText.leadingAnchor.constraint(equalTo: timeMessage.trailingAnchor, constant: 2).isActive = true
+//            }
+//            editedText.bottomAnchor.constraint(equalTo: containerMessage.bottomAnchor).isActive = true
+//        }
         topMarginText.isActive = true
         if dataMessages[indexPath.row]["attachment_flag"]  as? String ?? "" == "27" || dataMessages[indexPath.row]["attachment_flag"]  as? String ?? "" == "26" {
             messageText.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 85).isActive = true
@@ -4990,6 +5112,12 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                 timeMessage.textColor = .lightGray
             }
             timeMessage.font = UIFont.systemFont(ofSize: 10 + offset(), weight: .medium)
+            if dataMessages[indexPath.row][TypeDataMessage.last_edit] != nil && dataMessages[indexPath.row][TypeDataMessage.last_edit] as! Int64 != 0 {
+                timeMessage.text = (timeMessage.text ?? "") + "\n" + "Edited".localized()
+                if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+                    timeMessage.textAlignment = .right
+                }
+            }
         }
         
         let imageThumb = UIImageView()

+ 107 - 77
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -2259,8 +2259,11 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         if isContactCenter && fPinContacCenter.isEmpty && isRequestContactCenter {
             return
         }
+        if textFieldSend.isFirstResponder {
+            dismissKeyboard()
+        }
         DispatchQueue.main.async {
-            if (self.constraintBottomAttachment.constant == 0.0) {
+            if !self.viewSticker.isDescendant(of: self.view) {
                 self.constraintBottomAttachment.constant = 200.0
                 self.view.addSubview(self.viewSticker)
                 self.viewSticker.translatesAutoresizingMaskIntoConstraints = false
@@ -2318,7 +2321,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     }
     
     @objc func showChooserACKConfidential() {
-        dismissKeyboard()
+//        dismissKeyboard()
         let alertController = LibAlertController(title: "Message Mode".localized(), message: "Select".localized() + " " + "Message Mode".localized(), preferredStyle: .actionSheet)
         let imageConfidential = resizeImage(image: UIImage(named: "pb_icon_conf_msg_on", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal)
         let imageAck = resizeImage(image: UIImage(named: "pb_icon_ack_msg_on", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal)
@@ -2377,7 +2380,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         alertController.addAction(confidentialAction)
         alertController.addAction(ackAction)
         alertController.addAction(secretAction)
-        alertController.addAction(stickerAction)
+//        alertController.addAction(stickerAction)
         alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: { (UIAlertAction) in
             self.isConfidential = false
             self.isAck = false
@@ -2572,7 +2575,8 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             let info:NSDictionary = notification.userInfo! as NSDictionary
             let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
             
-            self.constraintViewTextField.constant = 0
+//            self.constraintViewTextField.constant = 0
+            self.constraintBottomAttachment.constant = 0
             self.constraintBottomContainerMultpileSelectSession.constant = 0
             UIView.animate(withDuration: TimeInterval(duration), animations: {
                 self.view.layoutIfNeeded()
@@ -2582,11 +2586,11 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     
     @objc func keyboardWillShow(notification: NSNotification) {
         if self.viewIfLoaded?.window != nil && !isEditingMessage {
-            if (self.constraintBottomAttachment.constant != 0.0) {
-                self.constraintBottomAttachment.constant = 0.0
-                self.viewSticker.removeConstraints(self.viewSticker.constraints)
-                self.viewSticker.removeFromSuperview()
-            }
+//            if (self.constraintBottomAttachment.constant != 0.0) {
+//                self.constraintBottomAttachment.constant = 0.0
+//                self.viewSticker.removeConstraints(self.viewSticker.constraints)
+//                self.viewSticker.removeFromSuperview()
+//            }
             let info:NSDictionary = notification.userInfo! as NSDictionary
             let keyboardSize = (info[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
             
@@ -2595,7 +2599,8 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
             
             if self.constraintViewTextField.constant != keyboardHeight - 60 {
-                self.constraintViewTextField.constant = keyboardHeight - 60
+//                self.constraintViewTextField.constant = keyboardHeight - 60
+                self.constraintBottomAttachment.constant = keyboardHeight
                 if isSearching {
                     self.constraintViewTextField.constant = self.constraintViewTextField.constant + 60
                     self.constraintBottomContainerMultpileSelectSession.constant = -keyboardHeight
@@ -3843,10 +3848,20 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
         let cursorPosition = textView.caretRect(for: nowTextFieldSend!.selectedTextRange!.start).origin
         let doubleCurrentLine = cursorPosition.y / nowTextFieldSend!.font!.lineHeight
         if doubleCurrentLine.isFinite {
-            let currentLine = Int(doubleCurrentLine)
+            let currentLine = Int(ceil(doubleCurrentLine))
             UIView.animate(withDuration: 0.3) {
-                let numberOfLines = textView.textContainer.lineBreakMode == .byWordWrapping ? Int(textView.contentSize.height / textView.font!.lineHeight) - 1 : 1
-                if currentLine == 0 && numberOfLines == 1 {
+                let layoutManager = textView.layoutManager
+                var numberOfLines = 0
+                var index = 0
+                let numberOfGlyphs = layoutManager.numberOfGlyphs
+
+                while index < numberOfGlyphs {
+                    var lineRange = NSRange()
+                    layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
+                    index = NSMaxRange(lineRange)
+                    numberOfLines += 1
+                }
+                if currentLine == 1 && (numberOfLines == 1 || numberOfLines == 0) {
                     if self.isEditingMessage {
                         self.constraintHeighteditTextView.constant = 40
                     } else {
@@ -3938,7 +3953,6 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
                 let newCursorPosition = cursorPosition + 2  // Adjust cursor position
                 textView.text = replacedText
                 textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
-                return
             }
         }
 
@@ -3954,34 +3968,10 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
                 let newCursorPosition = cursorPosition + 2  // Adjust cursor
                 textView.text = replacedText
                 textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
-                return
             }
         }
-
-        // Handle Undo: If user removes the first letter, revert back to original "- " or "1. "
-        let bulletUndoPattern = #"(^|\n)  • $"# // Matches "  • " when the letter is removed
-        if let match = text.range(of: bulletUndoPattern, options: .regularExpression) {
-            let replacedText = text.replacingOccurrences(of: "  • ", with: "- ", range: match)
-            let newCursorPosition = cursorPosition - 2
-            
-            textView.text = replacedText
-            textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
-            return
-        }
-
-        let numberUndoPattern = #"(^|\n)  (\d+)\. $"# // Matches "  1. " when the letter is removed
-        if let match = text.range(of: numberUndoPattern, options: .regularExpression) {
-            let replacedText = text.replacingOccurrences(of: "  ", with: "", range: match)
-            let newCursorPosition = cursorPosition - 2
-            
-            textView.text = replacedText
-            textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
-        }
         
-        textView.preserveCursorPosition(withChanges: { _ in
-            textView.attributedText = textView.text.richText(isEditing: true)
-            return .preserveCursor
-        })
+        handleRichText(textView)
     }
     
     private func checkLink(fullText: String) {
@@ -4186,6 +4176,18 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
     }
     
     public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
+        let indent = handleIndent(textView, range, text)
+        if !indent {
+            handleRichText(textView)
+            return indent
+        }
+        if (self.textFieldSend.text.count == 0) {
+            return text != "\n"
+        }
+        return true
+    }
+    
+    private func handleIndent(_ textView: UITextView, _ range: NSRange, _ text: String) -> Bool {
         guard let nsText = textView.text as NSString? else { return true }
         let newText = nsText.replacingCharacters(in: range, with: text)
         var lines = newText.components(separatedBy: "\n")
@@ -4245,13 +4247,16 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
             textView.selectedRange = NSRange(location: range.location - 1, length: 0)
             return false
         }
-        
-        if (self.textFieldSend.text.count == 0) {
-            return text != "\n"
-        }
         return true
     }
     
+    private func handleRichText(_ textView: UITextView) {
+        textView.preserveCursorPosition(withChanges: { _ in
+            textView.attributedText = textView.text.richText(isEditing: true)
+            return .preserveCursor
+        })
+    }
+    
     func isGIFData(_ data: Data) -> Bool {
         let gifSignature: [UInt8] = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]
         let rawData = [UInt8](data.prefix(6))
@@ -4697,8 +4702,8 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
             view.addSubview(blurView)
             blurView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor)
             
-            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissEditVC))
-            tapGesture.cancelsTouchesInView = false
+            let tapGesture = ObjectGesture(target: self, action: #selector(dismissEditVC))
+            tapGesture.message_id = oldText
             view.addGestureRecognizer(tapGesture)
             
             editTextView = CustomTextView()
@@ -4718,7 +4723,7 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
             constraintHeighteditTextView = editTextView.heightAnchor.constraint(equalToConstant: 40)
             constraintBottomeditTextView.isActive = true
             constraintHeighteditTextView.isActive = true
-            editTextView.text = oldText
+            editTextView.attributedText = oldText.richText(isEditing: true)
             editTextView.becomeFirstResponder()
             
             buttonSendEdit.setImage(resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "Send-(White)", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(.blackDarkMode) : UIImage(named: "Send-(White)", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal), for: .normal)
@@ -4752,6 +4757,8 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
                         self.tableChatView.reloadRows(at: [indexPath], with: .none)
                     }
                 }
+                self.isEditingMessage = false
+                self.editVC.dismiss(animated: true)
              })
             buttonSendEdit.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .mainColor
             view.addSubview(buttonSendEdit)
@@ -4790,7 +4797,7 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
             messageText.trailingAnchor.constraint(equalTo: viewMessage.trailingAnchor, constant: -15).isActive = true
             messageText.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
             messageText.font = .systemFont(ofSize: 12 + offset())
-            messageText.text = oldText
+            messageText.attributedText = oldText.richText()
         }
         editVC.modalTransitionStyle = .crossDissolve
         editVC.modalPresentationStyle = .overFullScreen
@@ -4802,9 +4809,21 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
         })
     }
     
-    @objc func dismissEditVC() {
-        self.isEditingMessage = false
-        editVC.dismiss(animated: true)
+    @objc func dismissEditVC(_ sender: ObjectGesture) {
+        if editTextView.text == sender.message_id {
+            self.isEditingMessage = false
+            editVC.dismiss(animated: true)
+        } else if self.isEditingMessage {
+            let alert = LibAlertController(title: "".localized(), message: "Discard edit?".localized(), preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: "Cancel".localized(), style: UIAlertAction.Style.cancel, handler: nil))
+            alert.addAction(UIAlertAction(title: "Discard".localized(), style: UIAlertAction.Style.default, handler: {(_) in
+                self.isEditingMessage = false
+                self.editVC.dismiss(animated: true)
+            }))
+            editVC.present(alert, animated: true, completion: nil)
+        } else {
+            editVC.dismiss(animated: true)
+        }
     }
     
     @objc func cancelAction() {
@@ -5701,7 +5720,6 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 containerButton.widthAnchor.constraint(equalToConstant: self.view!.frame.size.width * 0.9).isActive = true
                 containerButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 55).isActive = true
                 containerButton.backgroundColor = .clear
-//                timeMessage.bottomAnchor.constraint(equalTo:containerButton.topAnchor, constant: -5).isActive = true
                 
                 
                 for i in 0..<category_cc.count {
@@ -5868,12 +5886,13 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
         containerMessage.translatesAutoresizingMaskIntoConstraints = false
         
         let timeMessage = UILabel()
+        timeMessage.numberOfLines = 0
         cell.contentView.addSubview(timeMessage)
         timeMessage.translatesAutoresizingMaskIntoConstraints = false
         if (dataMessages[indexPath.row]["read_receipts"] as? String) == "8" || ((dataMessages[indexPath.row]["credential"] as? String) == "1" && dataMessages[indexPath.row]["lock"] as? String != "2") {
-            timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40 - 20).isActive = true
+            timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40).isActive = true
         } else {
-            timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5 - 20).isActive = true
+            timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5).isActive = true
         }
         
         let statusMessage = UIImageView()
@@ -5928,9 +5947,9 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
         if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
             containerMessage.leadingAnchor.constraint(greaterThanOrEqualTo: cell.contentView.leadingAnchor, constant: 60).isActive = true
             if (dataMessages[indexPath.row]["read_receipts"] as? String) == "8" || ((dataMessages[indexPath.row]["credential"] as? String) == "1" && dataMessages[indexPath.row]["lock"] as? String != "2") {
-                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40).isActive = true
             } else {
-                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5).isActive = true
             }
             if isContactCenter {
                 containerMessage.topAnchor.constraint(equalTo: nameSender.bottomAnchor).isActive = true
@@ -6026,9 +6045,9 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 }
             }
             if (dataMessages[indexPath.row]["read_receipts"] as? String) == "8" || ((dataMessages[indexPath.row]["credential"] as? String) == "1" && dataMessages[indexPath.row]["lock"] as? String != "2") {
-                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40).isActive = true
             } else {
-                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5).isActive = true
             }
             containerMessage.trailingAnchor.constraint(lessThanOrEqualTo: cell.contentView.trailingAnchor, constant: -60).isActive = true
             containerMessage.widthAnchor.constraint(greaterThanOrEqualToConstant: 46).isActive = true
@@ -6103,20 +6122,20 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
             }
         }
         
-        if dataMessages[indexPath.row][TypeDataMessage.last_edit] != nil && dataMessages[indexPath.row][TypeDataMessage.last_edit] as! Int64 != 0 {
-            let editedText = UILabel()
-            editedText.text = "Edited".localized()
-            editedText.font = UIFont.systemFont(ofSize: 10 + offset(), weight: .medium)
-            editedText.textColor = .lightGray
-            cell.contentView.addSubview(editedText)
-            editedText.translatesAutoresizingMaskIntoConstraints = false
-            if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
-                editedText.trailingAnchor.constraint(equalTo: timeMessage.leadingAnchor, constant: -2).isActive = true
-            } else {
-                editedText.leadingAnchor.constraint(equalTo: timeMessage.trailingAnchor, constant: 2).isActive = true
-            }
-            editedText.bottomAnchor.constraint(equalTo: containerMessage.bottomAnchor).isActive = true
-        }
+//        if dataMessages[indexPath.row][TypeDataMessage.last_edit] != nil && dataMessages[indexPath.row][TypeDataMessage.last_edit] as! Int64 != 0 {
+//            let editedText = UILabel()
+//            editedText.text = "Edited".localized()
+//            editedText.font = UIFont.systemFont(ofSize: 10 + offset(), weight: .medium)
+//            editedText.textColor = .lightGray
+//            cell.contentView.addSubview(editedText)
+//            editedText.translatesAutoresizingMaskIntoConstraints = false
+//            if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+//                editedText.trailingAnchor.constraint(equalTo: timeMessage.leadingAnchor, constant: -2).isActive = true
+//            } else {
+//                editedText.leadingAnchor.constraint(equalTo: timeMessage.trailingAnchor, constant: 2).isActive = true
+//            }
+//            editedText.bottomAnchor.constraint(equalTo: containerMessage.bottomAnchor).isActive = true
+//        }
         
         let messageText = UITextView()
         messageText.isEditable = false
@@ -6469,6 +6488,12 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 timeMessage.textColor = .lightGray
             }
             timeMessage.font = UIFont.systemFont(ofSize: 10 + offset(), weight: .medium)
+            if dataMessages[indexPath.row][TypeDataMessage.last_edit] != nil && dataMessages[indexPath.row][TypeDataMessage.last_edit] as! Int64 != 0 {
+                timeMessage.text = (timeMessage.text ?? "") + "\n" + "Edited".localized()
+                if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+                    timeMessage.textAlignment = .right
+                }
+            }
         }
         
         let imageThumb = UIImageView()
@@ -8069,14 +8094,19 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
 //        if copySession || forwardSession || deleteSession {
 //            return nil
 //        }
-//        let action = UIContextualAction(style: .normal,
-//                                        title: "") { [weak self] (action, view, completionHandler) in
-//                                            self?.handleReply(indexPath: indexPath)
-//                                            completionHandler(true)
+//        let action = UIContextualAction(style: .normal, title: "Reply") { [weak self] (action, view, completionHandler) in
+//            let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
+//            feedbackGenerator.impactOccurred()
+//
+//            self?.handleReply(indexPath: indexPath)
+//            completionHandler(true)
 //        }
+//        action.title = nil
 //        action.backgroundColor = .white
-//        action.image = UIImage(systemName: "arrowshape.turn.up.left.fill")?.withTintColor(.black, renderingMode: .alwaysOriginal)
-//        return UISwipeActionsConfiguration(actions: [action])
+//        action.image = UIImage(systemName: "arrowshape.turn.up.left.circle.fill")?.withTintColor(.gray, renderingMode: .alwaysOriginal)
+//        let config = UISwipeActionsConfiguration(actions: [action])
+//        config.performsFirstActionWithFullSwipe = false
+//        return config
 //    }
     
     private func handleReply(indexPath: IndexPath, dataMessagesImage: [String: Any?] = [:], reffId: String = "") {

+ 5 - 5
NexilisLite/NexilisLite/Source/View/Chat/MessageInfo.swift

@@ -63,7 +63,7 @@ class MessageInfo: UIViewController, UITableViewDelegate, UITableViewDataSource,
     private func getData() {
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
-                if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, status, time_delivered, time_read, time_ack, longitude, latitude FROM MESSAGE_STATUS where message_id='\(data["message_id"]!!)'") {
+                if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, status, time_delivered, time_read, time_ack, longitude, latitude FROM MESSAGE_STATUS where message_id='\(data["message_id"]!!)' ORDER BY time_delivered DESC, time_read DESC, time_ack DESC") {
                     var listStatus: [Int] = []
                     while cursorData.next() {
                         var row: [String: Any?] = [:]
@@ -555,17 +555,17 @@ class MessageInfo: UIViewController, UITableViewDelegate, UITableViewDataSource,
             cell.contentView.addSubview(timeMessage)
             timeMessage.translatesAutoresizingMaskIntoConstraints = false
             if (data["read_receipts"] as? String) == "8" {
-                timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40 - 20).isActive = true
+                timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40).isActive = true
             } else {
-                timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5 - 20).isActive = true
+                timeMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5).isActive = true
             }
             
             let statusMessage = UIImageView()
             containerMessage.leadingAnchor.constraint(greaterThanOrEqualTo: cell.contentView.leadingAnchor, constant: 60).isActive = true
             if (data["read_receipts"] as? String) == "8" {
-                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -40).isActive = true
             } else {
-                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5 - 20).isActive = true
+                containerMessage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -5).isActive = true
             }
             containerMessage.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 5).isActive = true
             containerMessage.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -15).isActive = true

+ 11 - 9
NexilisLite/NexilisLite/Source/View/Control/ContactChatViewController.swift

@@ -755,17 +755,19 @@ extension ContactChatViewController {
         }
         data.isSelected = !data.isSelected
         if data.isSelected {
-            for dataSubChat in self.chatGroupMaps[data.groupId]! {
-                if var indexParent = chats.firstIndex(where: { $0.isParent && $0.groupId == data.groupId }) {
-                    if isFilltering {
-                        fillteredData.insert(dataSubChat, at: indexParent + 1)
-                        indexParent+=1
-                    } else {
-                        chats.insert(dataSubChat, at: indexParent + 1)
-                        indexParent+=1
+            if let dataSubChats = self.chatGroupMaps[data.groupId] {
+                for dataSubChat in dataSubChats {
+                    if var indexParent = chats.firstIndex(where: { $0.isParent && $0.groupId == data.groupId }) {
+                        if isFilltering {
+                            fillteredData.insert(dataSubChat, at: indexParent + 1)
+                            indexParent+=1
+                        } else {
+                            chats.insert(dataSubChat, at: indexParent + 1)
+                            indexParent+=1
+                        }
                     }
+                    
                 }
-                
             }
         } else {
             if isFilltering {

+ 8 - 11
NexilisLite/NexilisLite/Source/View/Streaming/QmeraStreamingViewController.swift

@@ -8,6 +8,7 @@
 import UIKit
 import nuSDKService
 import NotificationBannerSwift
+import AVFAudio
 
 class QmeraStreamingViewController: UIViewController {
     
@@ -644,17 +645,13 @@ extension QmeraStreamingViewController: LiveStreamingDelegate {
                     self.imageView.transform = CGAffineTransform.init(scaleX: 1.9, y: 2.2).rotated(by: camera == 1 ? (CGFloat.pi * 5)/2 : (CGFloat.pi)/2)
                 }
             }
-            DispatchQueue.global().asyncAfter(deadline: .now() + 1, execute: {
-                API.adjustVolume(fValue: 10.0)
-                var bAudioEngineIsAvtive: Bool! = false
-                repeat {
-                    API.turnSpeakerPhone(bSPon: true)
-                    Thread.sleep(forTimeInterval: 1)
-                    bAudioEngineIsAvtive = API.bAudioEngineIsRunning()
-                    if (bAudioEngineIsAvtive) {
-                        break
-                    }
-                } while (!bAudioEngineIsAvtive)
+            DispatchQueue.global().asyncAfter(deadline: .now() + 0.5, execute: {
+                do {
+                    let audioSession = AVAudioSession.sharedInstance()
+                    try audioSession.overrideOutputAudioPort(.speaker)
+                } catch {
+                    
+                }
             })
             sendJoin()
         } else if state == Nexilis.AUDIO_CALL_RINGING {