浏览代码

update fix bugs and release for 5.0.68

alqindiirsyam 2 天之前
父节点
当前提交
81649acd48

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

@@ -568,7 +568,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.67;
+				MARKETING_VERSION = 5.0.68;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -604,7 +604,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.67;
+				MARKETING_VERSION = 5.0.68;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -640,7 +640,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.67;
+				MARKETING_VERSION = 5.0.68;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -679,7 +679,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.67;
+				MARKETING_VERSION = 5.0.68;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 27 - 0
AppBuilder/AppBuilder/FirstTabViewController.swift

@@ -16,6 +16,7 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
     var webView: WKWebView!
     var address = ""
     private var lastContentOffset: CGFloat = 0
+    var progressView: UIProgressView!
     
     var isAllowSpeech = false
     
@@ -74,6 +75,23 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         webView.navigationDelegate = self
         webView.allowsBackForwardNavigationGestures = true
         
+        progressView = UIProgressView(progressViewStyle: .default)
+        progressView.sizeToFit()
+        progressView.tintColor = .systemBlue
+        view.addSubview(progressView)
+        
+        // Auto Layout for progress bar (stick to top)
+        progressView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            progressView.heightAnchor.constraint(equalToConstant: 4)
+        ])
+        
+        // Observe estimatedProgress
+        webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
+        
         let contentController = self.webView.configuration.userContentController
         contentController.add(self, name: "checkProfile")
         contentController.add(self, name: "setIsProductModalOpen")
@@ -84,6 +102,7 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         contentController.add(self, name: "tabShowHide")
         contentController.add(self, name: "shareText")
         contentController.add(self, name: "openGalleryiOS")
+        contentController.add(self, name: "openChannel")
         
         let source: String = "var meta = document.createElement('meta');" +
             "meta.name = 'viewport';" +
@@ -110,6 +129,14 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         processURL()
     }
     
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "estimatedProgress" {
+            progressView.progress = Float(webView.estimatedProgress)
+            
+            progressView.isHidden = webView.estimatedProgress >= 1
+        }
+    }
+    
     func loadContentBlocker(into config: WKWebViewConfiguration, completion: @escaping () -> Void) {
         // Define ad-blocking rules directly in Swift as a string
         let contentRules = PrefsUtil.contentRulesAds

+ 3 - 0
AppBuilder/AppBuilder/Info.plist

@@ -115,6 +115,9 @@
 		<string>readdle-spark</string>
 		<string>ymail</string>
 		<string>comgooglemaps</string>
+        <string>instagram</string>
+        <string>twitter</string>
+        <string>youtube</string>
 	</array>
 	<key>LSRequiresIPhoneOS</key>
 	<true/>

+ 36 - 6
AppBuilder/AppBuilder/StatusUpdateVC.swift

@@ -15,6 +15,7 @@ class StatusUpdateVC: UIViewController, UIScrollViewDelegate, UIGestureRecognize
     var webView: WKWebView!
     var address = ""
     private var lastContentOffset: CGFloat = 0
+    var progressView: UIProgressView!
     
     var isAllowSpeech = false
     
@@ -73,6 +74,23 @@ class StatusUpdateVC: UIViewController, UIScrollViewDelegate, UIGestureRecognize
         webView.navigationDelegate = self
         webView.allowsBackForwardNavigationGestures = true
         
+        progressView = UIProgressView(progressViewStyle: .default)
+        progressView.sizeToFit()
+        progressView.tintColor = .systemBlue
+        view.addSubview(progressView)
+        
+        // Auto Layout for progress bar (stick to top)
+        progressView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            progressView.heightAnchor.constraint(equalToConstant: 4)
+        ])
+        
+        // Observe estimatedProgress
+        webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
+        
         let contentController = self.webView.configuration.userContentController
         contentController.add(self, name: "checkProfile")
         contentController.add(self, name: "setIsProductModalOpen")
@@ -83,6 +101,7 @@ class StatusUpdateVC: UIViewController, UIScrollViewDelegate, UIGestureRecognize
         contentController.add(self, name: "tabShowHide")
         contentController.add(self, name: "shareText")
         contentController.add(self, name: "openGalleryiOS")
+        contentController.add(self, name: "openChannel")
         
         let source: String = "var meta = document.createElement('meta');" +
             "meta.name = 'viewport';" +
@@ -105,10 +124,19 @@ class StatusUpdateVC: UIViewController, UIScrollViewDelegate, UIGestureRecognize
         NotificationCenter.default.addObserver(self, selector: #selector(onShowAC(notification:)), name: NSNotification.Name(rawValue: "onShowAC"), object: nil)
         NotificationCenter.default.addObserver(self, selector: #selector(onRefreshWebView(notification:)), name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil)
         NotificationCenter.default.addObserver(self, selector: #selector(onResumeWebView(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil)
+        imageVideoPicker = ImageVideoPicker(presentationController: self, delegate: self)
         StatusUpdateVC.canLoadURL = true
         processURL()
     }
     
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "estimatedProgress" {
+            progressView.progress = Float(webView.estimatedProgress)
+            
+            progressView.isHidden = webView.estimatedProgress >= 1
+        }
+    }
+    
     func loadContentBlocker(into config: WKWebViewConfiguration, completion: @escaping () -> Void) {
         // Define ad-blocking rules directly in Swift as a string
         let contentRules = PrefsUtil.contentRulesAds
@@ -482,6 +510,7 @@ class StatusUpdateVC: UIViewController, UIScrollViewDelegate, UIGestureRecognize
             let activityViewController = UIActivityViewController(activityItems: [param1], applicationActivities: nil)
             self.present(activityViewController, animated: true, completion: nil)
         } else if message.name == "openGalleryiOS" {
+            print("GEGE: \(message.body)")
             guard let dict = message.body as? [String: AnyObject],
                   let param1 = dict["param1"] as? Int else {
                 return
@@ -517,12 +546,13 @@ class StatusUpdateVC: UIViewController, UIScrollViewDelegate, UIGestureRecognize
         if imagevideo != nil {
             let imageData = imagevideo! as! [UIImagePickerController.InfoKey : Any]
             if (imageData[.mediaType] as! String == "public.image") {
-                let compressedImage = (imageData[.originalImage] as! UIImage).pngData()!
-                let base64String = compressedImage.base64EncodedString()
-                let base64ToWeb = "data:image/jpeg;base64,\(base64String)"
-                webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
-                    if let error = error {
-                        print("Error executing JavaScript: \(error)")
+                if let compressedImage = (imageData[.originalImage] as! UIImage).jpegData(compressionQuality: 0.5) {
+                    let base64String = compressedImage.base64EncodedString()
+                    let base64ToWeb = "data:image/jpeg;base64,\(base64String)"
+                    webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
+                        if let error = error {
+                            print("Error executing JavaScript: \(error)")
+                        }
                     }
                 }
             } else {

+ 27 - 0
AppBuilder/AppBuilder/ThirdTabViewController.swift

@@ -16,6 +16,7 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
     var webView: WKWebView!
     var address = ""
     private var lastContentOffset: CGFloat = 0
+    var progressView: UIProgressView!
     
     var isAllowSpeech = false
     
@@ -74,6 +75,23 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         webView.navigationDelegate = self
         webView.allowsBackForwardNavigationGestures = true
         
+        progressView = UIProgressView(progressViewStyle: .default)
+        progressView.sizeToFit()
+        progressView.tintColor = .systemBlue
+        view.addSubview(progressView)
+        
+        // Auto Layout for progress bar (stick to top)
+        progressView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            progressView.heightAnchor.constraint(equalToConstant: 4)
+        ])
+        
+        // Observe estimatedProgress
+        webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
+        
         let contentController = self.webView.configuration.userContentController
         contentController.add(self, name: "checkProfile")
         contentController.add(self, name: "setIsProductModalOpen")
@@ -84,6 +102,7 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         contentController.add(self, name: "tabShowHide")
         contentController.add(self, name: "shareText")
         contentController.add(self, name: "openGalleryiOS")
+        contentController.add(self, name: "openChannel")
         
         let source: String = "var meta = document.createElement('meta');" +
             "meta.name = 'viewport';" +
@@ -112,6 +131,14 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         processURL()
     }
     
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "estimatedProgress" {
+            progressView.progress = Float(webView.estimatedProgress)
+            
+            progressView.isHidden = webView.estimatedProgress >= 1
+        }
+    }
+    
     func loadContentBlocker(into config: WKWebViewConfiguration, completion: @escaping () -> Void) {
         // Define ad-blocking rules directly in Swift as a string
         let contentRules = PrefsUtil.contentRulesAds

+ 1 - 1
NexilisLite/NexilisLite/Source/APIS.swift

@@ -1722,7 +1722,7 @@ public class APIS: NSObject {
     }
     
     static func ackAPN(id: String) {
-        DispatchQueue.global().async {
+        DispatchQueue.global(qos: .userInitiated).async {
 //            Nexilis.sendStateToServer(s: "send ack from apn")
 //            if API.nGetCLXConnState() == 0 {
 //                do {

+ 48 - 2
NexilisLite/NexilisLite/Source/Extension.swift

@@ -1003,6 +1003,11 @@ extension String {
         return String(self[startIndex ..< endIndex])
     }
     
+    public func substring(with nsRange: NSRange) -> String? {
+        guard let range = Range(nsRange, in: self) else { return nil }
+        return String(self[range])
+    }
+    
     static public func offset() -> CGFloat{
         guard let fontSize = Int(SecureUserDefaults.shared.value(forKey: "font_size") ?? "0") else { return 0 }
         return CGFloat(fontSize)
@@ -1040,6 +1045,10 @@ extension String {
         
         processMentions(in: finalText, groupID: group_id, isEditing: isEditing, listMentionInTextField: listMentionInTextField)
         
+        if !isEditing {
+            applyParagraphStyles(to: finalText, font: font)
+        }
+        
         if isSearching {
             highlightSearchText(in: finalText, searchText: textSearch)
         }
@@ -1094,6 +1103,37 @@ extension String {
             }
         }
     }
+    
+    private func applyParagraphStyles(to text: NSMutableAttributedString, font: UIFont) {
+        let fullString = text.string as NSString
+        let lines = fullString.components(separatedBy: .newlines)
+        var location = 0
+        
+        for line in lines {
+            let nsRange = NSRange(location: location, length: line.count)
+            
+            if line.trimmingCharacters(in: .whitespaces).hasPrefix("•") ||
+                line.trimmingCharacters(in: .whitespaces).range(of: #"^[0-9]+\."#, options: .regularExpression) != nil {
+                
+                let paragraphStyle = NSMutableParagraphStyle()
+                paragraphStyle.lineSpacing = 0
+                paragraphStyle.paragraphSpacing = 0
+                paragraphStyle.firstLineHeadIndent = 0
+                if line.trimmingCharacters(in: .whitespaces).hasPrefix("•") {
+                    paragraphStyle.headIndent = 12 + String.offset() - 1
+                } else {
+                    paragraphStyle.headIndent = 12 + String.offset()
+                }
+                
+                text.addAttributes([
+                    .font: font,
+                    .paragraphStyle: paragraphStyle
+                ], range: nsRange)
+            }
+            
+            location += line.count + 1 // +1 untuk newline
+        }
+    }
 
     private func processMentions(in text: NSMutableAttributedString, groupID: String, isEditing: Bool, listMentionInTextField: [User] = []) {
         let regex = try? NSRegularExpression(pattern: "@([\\w-]+)", options: [])
@@ -1949,6 +1989,9 @@ class DataCaptured: NSObject {
         }
     }
     static func sendLogMonitorAction() {
+        if !Nexilis.checkingAccess(key: "activity_monitoring") {
+            return
+        }
         var type = "1"
         var value = "1"
         if action == "TEXT_CHANGED" {
@@ -1956,16 +1999,19 @@ class DataCaptured: NSObject {
             value = textAction
         }
 //        print("sendLogMonitorAction: \(type) <><> \(textAction) <><> \(value)")
-//        _ = Nexilis.writeSync(message: CoreMessage_TMessageBank.getLogMonitor(type: type, label: textAction, value: value))
+        _ = Nexilis.writeSync(message: CoreMessage_TMessageBank.getLogMonitor(type: type, label: textAction, value: value))
     }
     
     static func sendLogMonitorActivity() {
+        if !Nexilis.checkingAccess(key: "activity_monitoring") {
+            return
+        }
         var act = actNC
         if !actVC.isEmpty {
             act = actVC
         }
 //        print("sendLogMonitorActivity: \(act)")
-//        _ = Nexilis.write(message: CoreMessage_TMessageBank.getLogActivity(pActivityClassName: act))
+        _ = Nexilis.write(message: CoreMessage_TMessageBank.getLogActivity(pActivityClassName: act))
     }
     
     static func sendErrorDLP(fileName: String, code: Int) {

+ 3 - 3
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -19,7 +19,7 @@ import CryptoKit
 import WebKit
 
 public class Nexilis: NSObject {
-    public static var cpaasVersion = "5.0.67"
+    public static var cpaasVersion = "5.0.68"
     public static var sAPIKey = ""
     
     public static var ADDRESS = ""
@@ -725,7 +725,7 @@ public class Nexilis: NSObject {
             if key == "sms" || key == "email" || key == "whatsapp" || key == "battery_optimization_force" || key == "backup_restore" || key == "check_sim_swap" || key == "admin_features" || key == "can_config_fb" || key == "friend_request_approval" || key == "authentication" || key == "sign_in_up_msisdn" || key == "sign_in_up_email" {
                 return false
             } else {
-                return true
+                return false
             }
         } else if let jsonArray = try? JSONSerialization.jsonObject(with: dataAccess.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
             if jsonArray[key] != nil {
@@ -734,7 +734,7 @@ public class Nexilis: NSObject {
                 if key == "sms" || key == "email" || key == "whatsapp" || key == "battery_optimization_force" || key == "backup_restore" || key == "check_sim_swap" || key == "admin_features" || key == "can_config_fb" || key == "friend_request_approval" || key == "authentication" || key == "sign_in_up_msisdn" || key == "sign_in_up_email" {
                     return false
                 } else {
-                    return true
+                    return false
                 }
             }
         }

+ 27 - 0
NexilisLite/NexilisLite/Source/View/BNIView/BNIBookingWebView.swift

@@ -17,6 +17,7 @@ public class BNIBookingWebView: UIViewController, WKNavigationDelegate, UIScroll
     public var customUrl = ""
     public var isSecureBrowser = false
     let textField = UITextField()
+    var progressView: UIProgressView!
     
     var isAllowSpeech = false
     
@@ -128,6 +129,23 @@ public class BNIBookingWebView: UIViewController, WKNavigationDelegate, UIScroll
         webView.allowsBackForwardNavigationGestures = true
         webView.scrollView.delegate = self
         
+        progressView = UIProgressView(progressViewStyle: .default)
+        progressView.sizeToFit()
+        progressView.tintColor = .systemBlue
+        view.addSubview(progressView)
+        
+        // Auto Layout for progress bar (stick to top)
+        progressView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            progressView.heightAnchor.constraint(equalToConstant: 4)
+        ])
+        
+        // Observe estimatedProgress
+        webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
+        
         let contentController = webView.configuration.userContentController
         contentController.add(self, name: "sendQueueBNI")
         contentController.add(self, name: "checkProfile")
@@ -140,6 +158,7 @@ public class BNIBookingWebView: UIViewController, WKNavigationDelegate, UIScroll
         contentController.add(self, name: "finishForm")
         contentController.add(self, name: "shareText")
         contentController.add(self, name: "openGalleryiOS")
+        contentController.add(self, name: "openChannel")
         
         let source: String = "var meta = document.createElement('meta');" +
             "meta.name = 'viewport';" +
@@ -206,6 +225,14 @@ public class BNIBookingWebView: UIViewController, WKNavigationDelegate, UIScroll
         }
     }
     
+    public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "estimatedProgress" {
+            progressView.progress = Float(webView.estimatedProgress)
+            
+            progressView.isHidden = webView.estimatedProgress >= 1
+        }
+    }
+    
     func loadContentBlocker(into config: WKWebViewConfiguration, completion: @escaping () -> Void) {
         // Define ad-blocking rules directly in Swift as a string
         let contentRules = #"""

+ 107 - 6
NexilisLite/NexilisLite/Source/View/Call/CallLogVC.swift

@@ -7,6 +7,7 @@
 
 import Foundation
 import UIKit
+import NotificationBannerSwift
 
 public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchResultsUpdating, UISearchBarDelegate {
     private let tableView = UITableView(frame: .zero, style: .plain)
@@ -15,6 +16,16 @@ public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDele
     private var calls: [CallModel] = []
     private let textCallEmpty = UILabel()
     
+    private var filteredCalls: [CallModel] = []
+    
+    private var isSearchBarEmpty: Bool {
+        return searchController.searchBar.text!.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+    }
+    
+    private var isFilltering: Bool {
+        return searchController.isActive && !isSearchBarEmpty
+    }
+    
     public override func viewDidLoad() {
         tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellCallLog")
         tableView.dataSource = self
@@ -29,7 +40,6 @@ public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDele
         }
         
         setupTableView()
-        refresh()
         NotificationCenter.default.addObserver(self, selector: #selector(onRefreshCallLog(notification:)), name: NSNotification.Name(rawValue: "refreshCallLog"), object: nil)
     }
     
@@ -94,6 +104,8 @@ public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDele
         DispatchQueue.main.async {
             self.navigationController?.navigationBar.sizeToFit()
         }
+        
+        refresh()
     }
     
     public override func viewDidAppear(_ animated: Bool) {
@@ -110,7 +122,6 @@ public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDele
                 textCallEmpty.removeFromSuperview()
             }
             searchController.searchBar.isHidden = false
-            tableView.reloadData()
         } else {
             searchController.searchBar.isHidden = true
             textCallEmpty.numberOfLines = 0
@@ -126,6 +137,7 @@ public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDele
             view.addSubview(textCallEmpty)
             textCallEmpty.anchor(left: view.leftAnchor, right: view.rightAnchor, paddingLeft: 20, paddingRight: 20, centerX: view.centerXAnchor, centerY: view.centerYAnchor)
         }
+        tableView.reloadData()
     }
     
     @objc func onRefreshCallLog(notification: NSNotification) {
@@ -189,7 +201,7 @@ public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDele
                         }
                     }
                     if dataPin != nil {
-                        tempCall.append(CallModel(fPin: fPin, name: dataPin!.fullName, image: dataPin!.thumb, time: timeCall, isVideo: text.lowercased().contains("audio") ? false : true, status: statusCall))
+                        tempCall.append(CallModel(fPin: pin, name: dataPin!.fullName, image: dataPin!.thumb, time: timeCall, isVideo: text.lowercased().contains("audio") ? false : true, status: statusCall))
                     }
                 }
                 calls = tempCall
@@ -250,8 +262,73 @@ public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDele
         return calls.count
     }
     
-    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+    public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        tableView.deselectRow(at: indexPath, animated: true)
         let call = calls[indexPath.row]
+        if call.isVideo {
+            if !Nexilis.checkingAccess(key: "video_call") {
+                self.view.makeToast("Feature disabled..".localized(), duration: 3)
+                return
+            }
+            let goAudioCall = Nexilis.checkMicPermission()
+            let goVideoCall = Nexilis.checkCameraPermission()
+            if goVideoCall == 0 {
+                let alert = LibAlertController(title: "Attention!".localized(), message: !goAudioCall && goVideoCall == 0 ? "Please allow microphone & camera permission in your settings".localized() : !goAudioCall ? "Please allow microphone permission in your settings".localized() : "Please allow camera permission in your settings", preferredStyle: .alert)
+                alert.addAction(UIAlertAction(title: "OK".localized(), style: UIAlertAction.Style.default, handler: {_ in
+                    if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
+                        UIApplication.shared.open(url, options: [:], completionHandler: nil)
+                    }
+                }))
+                self.navigationController?.present(alert, animated: true, completion: nil)
+                return
+            } else if goVideoCall == -1 {
+                return
+            }
+            if !CheckConnection.isConnectedToNetwork() {
+                let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                imageView.tintColor = .white
+                let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                banner.show()
+                return
+            }
+            if let user = User.getDataCanNil(pin: call.fPin) {
+                let videoVC = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "videoVCQmera") as! QmeraVideoViewController
+                videoVC.fPin = user.pin
+                self.show(videoVC, sender: nil)
+            }
+        } else {
+            if !Nexilis.checkingAccess(key: "audio_call") {
+                self.view.makeToast("Feature disabled..".localized(), duration: 3)
+                return
+            }
+            if !CheckConnection.isConnectedToNetwork() {
+                let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                imageView.tintColor = .white
+                let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                banner.show()
+                return
+            }
+            if let user = User.getDataCanNil(pin: call.fPin) {
+                let controller = QmeraAudioViewController()
+                controller.user = user
+                controller.isOutgoing = true
+                controller.modalPresentationStyle = .overCurrentContext
+                let navigationController = CustomNavigationController(rootViewController: controller)
+                navigationController.modalPresentationStyle = .fullScreen
+                if UIApplication.shared.visibleViewController?.navigationController != nil {
+                    UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+                } else {
+                    UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+                }
+            }
+        }
+    }
+    
+    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        var call = calls[indexPath.row]
+        if isFilltering {
+            call = filteredCalls[indexPath.row]
+        }
         let cell = tableView.dequeueReusableCell(withIdentifier: "cellCallLog", for: indexPath)
         cell.backgroundColor = .clear
         let textTime = UILabel()
@@ -263,6 +340,7 @@ public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDele
         textTime.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         
         let detailButton = UIButton(type: .detailDisclosure)
+        detailButton.restorationIdentifier = call.fPin
         detailButton.addTarget(self, action: #selector(detailButtonTapped), for: .touchUpInside)
 
         let stack = UIStackView(arrangedSubviews: [textTime, detailButton])
@@ -307,11 +385,34 @@ public class CallLogVC: UIViewController, UITableViewDataSource, UITableViewDele
         return cell
     }
     
-    @objc func detailButtonTapped() {
-        print("Detail button tapped")
+    @objc func detailButtonTapped(_ sender: UIButton) {
+        if(Nexilis.checkIsChangePerson()){
+            if let id = sender.restorationIdentifier {
+                if let data = User.getDataCanNil(pin: id) {
+                    let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "profileView") as! ProfileViewController
+                    controller.flag = .friend
+                    controller.user = data
+                    controller.name = data.fullName
+                    controller.data = id
+                    controller.picture = data.thumb
+                    self.navigationController?.navigationBar.prefersLargeTitles = false
+                    self.navigationController?.navigationItem.largeTitleDisplayMode = .never
+                    self.navigationController?.show(controller, sender: nil)
+                }
+            }
+        }
     }
     
     public func updateSearchResults(for searchController: UISearchController) {
+        filterContentForSearchText(searchController.searchBar.text!.trimmingCharacters(in: .whitespacesAndNewlines))
+    }
+    
+    private func filterContentForSearchText(_ searchText: String) {
+        filteredCalls = calls.filter({ d in
+            let name = d.name.trimmingCharacters(in: .whitespaces)
+            return name.lowercased().contains(searchText.lowercased())
+        })
+        tableView.reloadData()
     }
     
     private func typeCallLog(type: String, isVideo: Bool) -> NSMutableAttributedString {

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

@@ -82,6 +82,29 @@ class CustomTextView: UITextView {
                 let dataGif = UIPasteboard.general.data(forPasteboardType: "com.compuserve.gif")
                 customDelegate?.customTextViewDidPasteText(image: pasteboardItems["public.png"] as? UIImage ?? pasteboardItems["public.jpeg"] as? UIImage, dataGIF: dataGif)
                 return
+            } else if let string = UIPasteboard.general.string {
+                var formattedText = string
+                // Replace "- " only if it starts a line (after \n or at beginning)
+                if let bulletRegex = try? NSRegularExpression(pattern: #"(?m)^(?:- |\* |• )"#) {
+                    formattedText = bulletRegex.stringByReplacingMatches(
+                        in: formattedText,
+                        options: [],
+                        range: NSRange(location: 0, length: formattedText.utf16.count),
+                        withTemplate: "  • "
+                    )
+                }
+
+                // Replace numbered lists "1.", "2.", etc. only if they start a line
+                if let numberRegex = try? NSRegularExpression(pattern: #"(?m)^(\d+)\."#) {
+                    formattedText = numberRegex.stringByReplacingMatches(
+                        in: formattedText,
+                        options: [],
+                        range: NSRange(location: 0, length: formattedText.utf16.count),
+                        withTemplate: "  $1."
+                    )
+                }
+                self.replace(self.selectedTextRange!, withText: formattedText)
+                return
             }
         } else if let string = UIPasteboard.general.string {
             self.replace(self.selectedTextRange!, withText: string)

+ 92 - 57
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -628,15 +628,15 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         Database.shared.database?.inTransaction({ fmdb, rollback in
             if let c = Database().getRecords(fmdb: fmdb, query: "select first_name || ' ' || last_name, image_id from BUDDY where f_pin = '\(f_pin)'"), c.next() {
                 data["name"] = c.string(forColumnIndex: 0)!.trimmingCharacters(in: .whitespacesAndNewlines)
-                data["image_id"] = c.string(forColumnIndex: 1)!
+                data["image_id"] = c.string(forColumnIndex: 1) ?? ""
                 c.close()
             }
             else if let c = Database().getRecords(fmdb: fmdb, query: "select first_name || ' ' || last_name, thumb_id from GROUPZ_MEMBER where f_pin = '\(f_pin)' AND group_id = '\(dataGroup["group_id"]!!)'"), c.next() {
                 data["name"] = c.string(forColumnIndex: 0)!.trimmingCharacters(in: .whitespacesAndNewlines)
-                data["image_id"] = c.string(forColumnIndex: 1)!
+                data["image_id"] = c.string(forColumnIndex: 1) ?? ""
                 c.close()
             } else if let c = Database().getRecords(fmdb: fmdb, query: "select f_display_name from MESSAGE where message_id = '\(message_id)'"), c.next() {
-                data["name"] = c.string(forColumnIndex: 0)!
+                data["name"] = c.string(forColumnIndex: 0) ?? ""
                 data["image_id"] = ""
                 c.close()
             } else if f_pin == "-997" {
@@ -1716,7 +1716,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
             controller.data = sender.message_id
             controller.flag = .me
             navigationController?.show(controller, sender: nil)
-        } else {
+        } else if sender.message_id != "-999" && sender.message_id != "-997"  {
             let data = User.getDataCanNil(pin: sender.message_id)
             if data != nil {
                 let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "profileView") as! ProfileViewController
@@ -1937,12 +1937,19 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
             reff_id = reffId!
         }
         var message_text = message_text
-        let bulletPoint = "  • "
-        let numberPattern = #"  \d+\.\ "#
-        let firstLine = message_text.components(separatedBy: .newlines).first ?? ""
+        message_text = message_text.replacingOccurrences(of: "\n  •", with: "\n•")
+        if message_text.hasPrefix("  •") {
+            message_text = message_text.replacingOccurrences(of: "  •", with: "•")
+        }
+        let regex = try! NSRegularExpression(pattern: #"(?m)^\s{2}([0-9]+\.)"#)
+        message_text = regex.stringByReplacingMatches(in: message_text,
+                                                     options: [],
+                                                     range: NSRange(location: 0, length: message_text.utf16.count),
+                                                     withTemplate: "$1")
+        
 
         // Check if text contains bullet points or numbered list using regex
-        if !message_text.isEmpty && !firstLine.contains(bulletPoint) && firstLine.range(of: numberPattern, options: .regularExpression) == nil {
+        if !message_text.isEmpty {
             message_text = message_text.trimmingCharacters(in: .whitespacesAndNewlines)
         }
         let idMe = User.getMyPin() as String?
@@ -3039,28 +3046,27 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
         
         //indention code:
         let text = textView.text ?? ""
-        let cursorPositionIndent = textView.selectedRange.location
+        let cursorLocation = textView.selectedRange.location
 
-        // Prevent moving cursor before the 2-space indent
-        var adjustedCursorPosition = cursorPositionIndent
-        
-        for line in lines {
-            if let range = text.range(of: line), NSRange(range, in: text).contains(cursorPositionIndent) {
-                if line.hasPrefix("  •") || line.range(of: #"^\s{2}\d+\."#, options: .regularExpression) != nil {
-                    let startOfLine = text.distance(from: text.startIndex, to: range.lowerBound)
-                    let minCursorPosition = startOfLine + 2 // Prevent cursor before indentation
-                    
-                    if cursorPositionIndent < minCursorPosition {
-                        adjustedCursorPosition = minCursorPosition
+        // Find current line range where cursor is
+        if let lineRange = (text as NSString).lineRange(for: NSRange(location: cursorLocation, length: 0)) as NSRange? {
+            let line = (text as NSString).substring(with: lineRange)
+
+            // Detect bullet ("  •") or numbered ("  1.") list
+            if line.hasPrefix("  •") || line.range(of: #"^\s{2}\d+\."#, options: .regularExpression) != nil {
+                var bulletEnd = lineRange.location + 2
+                if !line.hasPrefix("  •") {
+                    bulletEnd = lineRange.location + 3
+                }
+
+                // Prevent cursor before bullet/number
+                if cursorLocation < bulletEnd {
+                    DispatchQueue.main.async {
+                        textView.selectedRange = NSRange(location: bulletEnd, length: 0)
                     }
                 }
-                break
             }
         }
-        
-        if adjustedCursorPosition != cursorPositionIndent {
-            textView.selectedRange = NSRange(location: adjustedCursorPosition, length: 0)
-        }
     }
     
     func extractFromAtIfSymbolsBefore(_ text: String) -> String? {
@@ -3135,7 +3141,9 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
 
                 let newCursorPosition = cursorPosition + 2  // Adjust cursor position
                 textView.text = replacedText
-                textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
+                DispatchQueue.main.async {
+                    textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
+                }
             }
         }
 
@@ -3144,12 +3152,11 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
         if let match = text.range(of: numberPattern, options: .regularExpression) {
             let matchedText = text[match]
 
-            if let spaceIndex = matchedText.firstIndex(of: " ") {
-                let firstLetter = matchedText[matchedText.index(after: spaceIndex)...]
-                let replacedText = text.replacingOccurrences(of: matchedText, with: "  \(matchedText)", range: match)
+            let replacedText = text.replacingOccurrences(of: matchedText, with: "  \(matchedText)", range: match)
 
-                let newCursorPosition = cursorPosition + 2  // Adjust cursor
-                textView.text = replacedText
+            let newCursorPosition = cursorPosition + 2  // Adjust cursor
+            textView.text = replacedText
+            DispatchQueue.main.async {
                 textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
             }
         }
@@ -3521,17 +3528,6 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
         guard affectedLineIndex >= 0, affectedLineIndex < lines.count else { return true }
         
         let affectedLine = lines[affectedLineIndex]
-        
-        // Prevent deleting two-space indentation before bullet/number
-        if affectedLine.hasPrefix("  •") || affectedLine.range(of: #"^\s{2}\d+\."#, options: .regularExpression) != nil {
-            if let lineStart = textView.text.range(of: affectedLine)?.lowerBound,
-               let startIndex = textView.text.distance(of: lineStart) {
-                if range.location == startIndex || range.location == startIndex + 1 {
-                    return false
-                }
-            }
-        }
-        
         // Auto-indent new lines based on previous line
         if text == "\n" {
             let previousLine = lines[affectedLineIndex]
@@ -3539,7 +3535,9 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
             if previousLine.hasPrefix("  •") {
                 let newBullet = "\n  • "
                 textView.text = nsText.replacingCharacters(in: range, with: newBullet)
-                textView.selectedRange = NSRange(location: range.location + newBullet.utf16.count, length: 0)
+                DispatchQueue.main.async {
+                    textView.selectedRange = NSRange(location: range.location + newBullet.utf16.count, length: 0)
+                }
                 return false
             }
             
@@ -3549,7 +3547,9 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
                 
                 let newNumber = "\n  \(number + 1). "
                 textView.text = nsText.replacingCharacters(in: range, with: newNumber)
-                textView.selectedRange = NSRange(location: range.location + newNumber.utf16.count, length: 0)
+                DispatchQueue.main.async {
+                    textView.selectedRange = NSRange(location: range.location + newNumber.utf16.count, length: 0)
+                }
                 return false
             }
         }
@@ -3558,25 +3558,31 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
         if text.isEmpty && affectedLine.trimmingCharacters(in: .whitespaces) == "•" {
             lines[affectedLineIndex] = "- "  // Replace "  • " with "- "
             textView.text = lines.joined(separator: "\n")
-            textView.selectedRange = NSRange(location: range.location - 1, length: 0)
+            DispatchQueue.main.async {
+                textView.selectedRange = NSRange(location: range.location - 1, length: 0)
+            }
+            return false
+        }
+        
+        // Handle Backspace on bullet
+        if text.isEmpty, newText.hasPrefix(" •"), newText.substring(with: NSRange(location: range.location - 1, length: 2)) == " •" {
             return false
         }
         
         // Handle Backspace on Numbered List
-        if text.isEmpty, affectedLine.range(of: #"^\s{2}(\d+)\.$"#, options: .regularExpression) != nil {
+        if text.isEmpty, newText.hasPrefix("  "), newText.substring(with: NSRange(location: range.location - 2, length: 2)) == "  " {
             lines[affectedLineIndex] = affectedLine.trimmingCharacters(in: .whitespaces)
             textView.text = lines.joined(separator: "\n")
-            textView.selectedRange = NSRange(location: range.location - 1, length: 0)
+            DispatchQueue.main.async {
+                textView.selectedRange = NSRange(location: range.location - 2, length: 0)
+            }
             return false
         }
         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 ?? "", listMentionInTextField: self.listMentionInTextField)
-            return .preserveCursor
-        })
+        textView.attributedText = textView.text.richText(isEditing: true, group_id: self.dataGroup["group_id"]  as? String ?? "", listMentionInTextField: self.listMentionInTextField)
     }
     
     public func textView(_ textView: UITextView, shouldInteractWith URL: URL?, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
@@ -6811,7 +6817,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                 containerReply.clipsToBounds = true
                 
                 if (thumbChat != "" || fileChat != "") && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"]  as? String ?? "" != "1") {
-                    topMarginText = messageText.topAnchor.constraint(greaterThanOrEqualTo: containerMessage.topAnchor, constant: topMarginText.constant + 50 + (self.offset()*3))
+                    topMarginText = messageText.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: topMarginText.constant + 50 + (self.offset()*3))
                 }
                 
                 let leftReply = UIView()
@@ -7627,11 +7633,40 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
         if stringURl.lowercased().starts(with: "www.") {
             stringURl = "https://" + stringURl.replacingOccurrences(of: "www.", with: "")
         }
-        if Nexilis.checkingAccess(key: "secure_browser") {
-            APIS.openUrl(url: stringURl)
-        } else {
-            guard let url = URL(string: stringURl) else { return }
-            UIApplication.shared.open(url)
+        let app: UIApplication = UIApplication.shared
+        var appURL: URL? = nil
+        if let url = URL(string: stringURl) {
+            if url.host?.contains("instagram.com") == true {
+                // Convert to Instagram deep link
+                if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
+                   let path = components.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
+                    appURL = URL(string: "instagram://\(path)")
+                }
+            } else if url.host?.contains("x.com") == true || url.host?.contains("twitter.com") == true {
+                if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
+                   let path = components.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
+                    appURL = URL(string: "twitter://\(path)")
+                }
+            } else if url.host?.contains("youtube.com") == true || url.host?.contains("youtu.be") == true {
+                appURL = URL(string: "youtube://\(url.absoluteString)")
+            }
+            if let appURL = appURL, app.canOpenURL(appURL) {
+                app.open(appURL, options: [:]) { success in
+                    if !success {
+                        if Nexilis.checkingAccess(key: "secure_browser") {
+                            APIS.openUrl(url: stringURl)
+                        } else {
+                            app.open(url)
+                        }
+                    }
+                }
+            } else {
+                if Nexilis.checkingAccess(key: "secure_browser") {
+                    APIS.openUrl(url: stringURl)
+                } else {
+                    app.open(url)
+                }
+            }
         }
     }
     

+ 95 - 59
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -966,14 +966,14 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             do {
                 if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
                     if cursorData.next() {
-                        dataPerson["f_pin"] = cursorData.string(forColumnIndex: 0)
-                        dataPerson["name"] = cursorData.string(forColumnIndex: 1)?.trimmingCharacters(in: .whitespaces)
-                        dataPerson["picture"] = cursorData.string(forColumnIndex: 3)
-                        dataPerson["isOfficial"] = cursorData.string(forColumnIndex: 2)
+                        dataPerson["f_pin"] = cursorData.string(forColumnIndex: 0) ?? ""
+                        dataPerson["name"] = (cursorData.string(forColumnIndex: 1) ?? "").trimmingCharacters(in: .whitespaces)
+                        dataPerson["picture"] = cursorData.string(forColumnIndex: 3) ?? ""
+                        dataPerson["isOfficial"] = cursorData.string(forColumnIndex: 2) ?? ""
                         if isContactCenter && isRequestContactCenter {
                             dataPerson["user_type"] = "0"
                         } else {
-                            dataPerson["user_type"] = cursorData.string(forColumnIndex: 6)
+                            dataPerson["user_type"] = cursorData.string(forColumnIndex: 6) ?? ""
                         }
                     } else {
                         dataPerson["f_pin"] = "-999"
@@ -2852,12 +2852,19 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             }
         }
         var message_text = message_text
-        let bulletPoint = "  • "
-        let numberPattern = #"  \d+\.\ "#
-        let firstLine = message_text.components(separatedBy: .newlines).first ?? ""
+        message_text = message_text.replacingOccurrences(of: "\n  •", with: "\n•")
+        if message_text.hasPrefix("  •") {
+            message_text = message_text.replacingOccurrences(of: "  •", with: "•")
+        }
+        let regex = try! NSRegularExpression(pattern: #"(?m)^\s{2}([0-9]+\.)"#)
+        message_text = regex.stringByReplacingMatches(in: message_text,
+                                                     options: [],
+                                                     range: NSRange(location: 0, length: message_text.utf16.count),
+                                                     withTemplate: "$1")
+        
 
         // Check if text contains bullet points or numbered list using regex
-        if !message_text.isEmpty && !firstLine.contains(bulletPoint) && firstLine.range(of: numberPattern, options: .regularExpression) == nil {
+        if !message_text.isEmpty {
             message_text = message_text.trimmingCharacters(in: .whitespacesAndNewlines)
         }
         
@@ -4449,30 +4456,30 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
                 buttonSendEdit.isEnabled = true
             }
         }
+        
         //indention code:
         let text = textView.text ?? ""
-        let cursorPositionIndent = textView.selectedRange.location
+        let cursorLocation = textView.selectedRange.location
 
-        // Prevent moving cursor before the 2-space indent
-        var adjustedCursorPosition = cursorPositionIndent
-        
-        for line in lines {
-            if let range = text.range(of: line), NSRange(range, in: text).contains(cursorPositionIndent) {
-                if line.hasPrefix("  •") || line.range(of: #"^\s{2}\d+\."#, options: .regularExpression) != nil {
-                    let startOfLine = text.distance(from: text.startIndex, to: range.lowerBound)
-                    let minCursorPosition = startOfLine + 2 // Prevent cursor before indentation
-                    
-                    if cursorPositionIndent < minCursorPosition {
-                        adjustedCursorPosition = minCursorPosition
+        // Find current line range where cursor is
+        if let lineRange = (text as NSString).lineRange(for: NSRange(location: cursorLocation, length: 0)) as NSRange? {
+            let line = (text as NSString).substring(with: lineRange)
+
+            // Detect bullet ("  •") or numbered ("  1.") list
+            if line.hasPrefix("  •") || line.range(of: #"^\s{2}\d+\."#, options: .regularExpression) != nil {
+                var bulletEnd = lineRange.location + 2
+                if !line.hasPrefix("  •") {
+                    bulletEnd = lineRange.location + 3
+                }
+
+                // Prevent cursor before bullet/number
+                if cursorLocation < bulletEnd {
+                    DispatchQueue.main.async {
+                        textView.selectedRange = NSRange(location: bulletEnd, length: 0)
                     }
                 }
-                break
             }
         }
-        
-        if adjustedCursorPosition != cursorPositionIndent {
-            textView.selectedRange = NSRange(location: adjustedCursorPosition, length: 0)
-        }
     }
     
     func extractFromAtIfSymbolsBefore(_ text: String) -> String? {
@@ -4509,7 +4516,6 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
         timerCheckLink = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: {_ in
             self.checkLink(fullText: textView.text)
         })
-        //indention code:
         let text = textView.text ?? ""
         let cursorPosition = textView.selectedRange.location
         
@@ -4533,6 +4539,7 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
             }
         }
         
+        //indention code:
         // Handle Bullets (- [space] + letter → • )
         let bulletPattern = #"(?<=\n|^)- (\S)"#
         if let match = text.range(of: bulletPattern, options: .regularExpression) {
@@ -4544,7 +4551,9 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
 
                 let newCursorPosition = cursorPosition + 2  // Adjust cursor position
                 textView.text = replacedText
-                textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
+                DispatchQueue.main.async {
+                    textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
+                }
             }
         }
 
@@ -4553,12 +4562,11 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
         if let match = text.range(of: numberPattern, options: .regularExpression) {
             let matchedText = text[match]
 
-            if let spaceIndex = matchedText.firstIndex(of: " ") {
-                let firstLetter = matchedText[matchedText.index(after: spaceIndex)...]
-                let replacedText = text.replacingOccurrences(of: matchedText, with: "  \(matchedText)", range: match)
+            let replacedText = text.replacingOccurrences(of: matchedText, with: "  \(matchedText)", range: match)
 
-                let newCursorPosition = cursorPosition + 2  // Adjust cursor
-                textView.text = replacedText
+            let newCursorPosition = cursorPosition + 2  // Adjust cursor
+            textView.text = replacedText
+            DispatchQueue.main.async {
                 textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
             }
         }
@@ -4934,17 +4942,6 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
         guard affectedLineIndex >= 0, affectedLineIndex < lines.count else { return true }
         
         let affectedLine = lines[affectedLineIndex]
-        
-        // Prevent deleting two-space indentation before bullet/number
-        if affectedLine.hasPrefix("  •") || affectedLine.range(of: #"^\s{2}\d+\."#, options: .regularExpression) != nil {
-            if let lineStart = textView.text.range(of: affectedLine)?.lowerBound,
-               let startIndex = textView.text.distance(of: lineStart) {
-                if range.location == startIndex || range.location == startIndex + 1 {
-                    return false
-                }
-            }
-        }
-        
         // Auto-indent new lines based on previous line
         if text == "\n" {
             let previousLine = lines[affectedLineIndex]
@@ -4952,7 +4949,9 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
             if previousLine.hasPrefix("  •") {
                 let newBullet = "\n  • "
                 textView.text = nsText.replacingCharacters(in: range, with: newBullet)
-                textView.selectedRange = NSRange(location: range.location + newBullet.utf16.count, length: 0)
+                DispatchQueue.main.async {
+                    textView.selectedRange = NSRange(location: range.location + newBullet.utf16.count, length: 0)
+                }
                 return false
             }
             
@@ -4962,7 +4961,9 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
                 
                 let newNumber = "\n  \(number + 1). "
                 textView.text = nsText.replacingCharacters(in: range, with: newNumber)
-                textView.selectedRange = NSRange(location: range.location + newNumber.utf16.count, length: 0)
+                DispatchQueue.main.async {
+                    textView.selectedRange = NSRange(location: range.location + newNumber.utf16.count, length: 0)
+                }
                 return false
             }
         }
@@ -4971,25 +4972,31 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
         if text.isEmpty && affectedLine.trimmingCharacters(in: .whitespaces) == "•" {
             lines[affectedLineIndex] = "- "  // Replace "  • " with "- "
             textView.text = lines.joined(separator: "\n")
-            textView.selectedRange = NSRange(location: range.location - 1, length: 0)
+            DispatchQueue.main.async {
+                textView.selectedRange = NSRange(location: range.location - 1, length: 0)
+            }
+            return false
+        }
+        
+        // Handle Backspace on bullet
+        if text.isEmpty, newText.hasPrefix(" •"), newText.substring(with: NSRange(location: range.location - 1, length: 2)) == " •" {
             return false
         }
         
         // Handle Backspace on Numbered List
-        if text.isEmpty, affectedLine.range(of: #"^\s{2}(\d+)\.$"#, options: .regularExpression) != nil {
+        if text.isEmpty, newText.hasPrefix("  "), newText.substring(with: NSRange(location: range.location - 2, length: 2)) == "  " {
             lines[affectedLineIndex] = affectedLine.trimmingCharacters(in: .whitespaces)
             textView.text = lines.joined(separator: "\n")
-            textView.selectedRange = NSRange(location: range.location - 1, length: 0)
+            DispatchQueue.main.async {
+                textView.selectedRange = NSRange(location: range.location - 2, length: 0)
+            }
             return false
         }
         return true
     }
     
     private func handleRichText(_ textView: UITextView) {
-        textView.preserveCursorPosition(withChanges: { _ in
-            textView.attributedText = textView.text.richText(isEditing: true, listMentionInTextField: self.listMentionInTextField)
-            return .preserveCursor
-        })
+        textView.attributedText = textView.text.richText(isEditing: true, listMentionInTextField: self.listMentionInTextField)
     }
     
     func isGIFData(_ data: Data) -> Bool {
@@ -8578,7 +8585,7 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 containerReply.clipsToBounds = true
                 
                 if (thumbChat != "" || fileChat != "") && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"]  as? String ?? "" != "1") {
-                    topMarginText = messageText.topAnchor.constraint(greaterThanOrEqualTo: containerMessage.topAnchor, constant: topMarginText.constant + 50 + (self.offset()*3))
+                    topMarginText = messageText.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: topMarginText.constant + 50 + (self.offset()*3))
                 }
                 
                 let leftReply = UIView()
@@ -9492,11 +9499,40 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
         if stringURl.lowercased().starts(with: "www.") {
             stringURl = "https://" + stringURl.replacingOccurrences(of: "www.", with: "")
         }
-        if Nexilis.checkingAccess(key: "secure_browser") {
-            APIS.openUrl(url: stringURl)
-        } else {
-            guard let url = URL(string: stringURl) else { return }
-            UIApplication.shared.open(url)
+        let app: UIApplication = UIApplication.shared
+        var appURL: URL? = nil
+        if let url = URL(string: stringURl) {
+            if url.host?.contains("instagram.com") == true {
+                // Convert to Instagram deep link
+                if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
+                   let path = components.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
+                    appURL = URL(string: "instagram://\(path)")
+                }
+            } else if url.host?.contains("x.com") == true || url.host?.contains("twitter.com") == true {
+                if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
+                   let path = components.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
+                    appURL = URL(string: "twitter://\(path)")
+                }
+            } else if url.host?.contains("youtube.com") == true || url.host?.contains("youtu.be") == true {
+                appURL = URL(string: "youtube://\(url.absoluteString)")
+            }
+            if let appURL = appURL, app.canOpenURL(appURL) {
+                app.open(appURL, options: [:]) { success in
+                    if !success {
+                        if Nexilis.checkingAccess(key: "secure_browser") {
+                            APIS.openUrl(url: stringURl)
+                        } else {
+                            app.open(url)
+                        }
+                    }
+                }
+            } else {
+                if Nexilis.checkingAccess(key: "secure_browser") {
+                    APIS.openUrl(url: stringURl)
+                } else {
+                    app.open(url)
+                }
+            }
         }
     }
     

+ 34 - 5
NexilisLite/NexilisLite/Source/View/Chat/EditorStarMessages.swift

@@ -1004,11 +1004,40 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
         if stringURl.lowercased().starts(with: "www.") {
             stringURl = "https://" + stringURl.replacingOccurrences(of: "www.", with: "")
         }
-        if Nexilis.checkingAccess(key: "secure_browser") {
-            APIS.openUrl(url: stringURl)
-        } else {
-            guard let url = URL(string: stringURl) else { return }
-            UIApplication.shared.open(url)
+        let app: UIApplication = UIApplication.shared
+        var appURL: URL? = nil
+        if let url = URL(string: stringURl) {
+            if url.host?.contains("instagram.com") == true {
+                // Convert to Instagram deep link
+                if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
+                   let path = components.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
+                    appURL = URL(string: "instagram://\(path)")
+                }
+            } else if url.host?.contains("x.com") == true || url.host?.contains("twitter.com") == true {
+                if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
+                   let path = components.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
+                    appURL = URL(string: "twitter://\(path)")
+                }
+            } else if url.host?.contains("youtube.com") == true || url.host?.contains("youtu.be") == true {
+                appURL = URL(string: "youtube://\(url.absoluteString)")
+            }
+            if let appURL = appURL, app.canOpenURL(appURL) {
+                app.open(appURL, options: [:]) { success in
+                    if !success {
+                        if Nexilis.checkingAccess(key: "secure_browser") {
+                            APIS.openUrl(url: stringURl)
+                        } else {
+                            app.open(url)
+                        }
+                    }
+                }
+            } else {
+                if Nexilis.checkingAccess(key: "secure_browser") {
+                    APIS.openUrl(url: stringURl)
+                } else {
+                    app.open(url)
+                }
+            }
         }
     }