alqindiirsyam пре 1 дан
родитељ
комит
f1752cf4b1

+ 101 - 4
NexilisLite/NexilisLite/Source/Utils.swift

@@ -508,7 +508,7 @@ public final class Utils {
 
             let imageString = NSAttributedString(attachment: imageAttachment)
             let textString = NSAttributedString(string: " " + textPreview, attributes: [
-                .font: UIFont.systemFont(ofSize: 14),
+                .font: UIFont.systemFont(ofSize: 14 + String.offset()),
                 .foregroundColor: UIColor.gray
             ])
             
@@ -571,7 +571,7 @@ public final class Utils {
     }
     
     private static func showNSMutableAttributedString(_ text: String) -> NSMutableAttributedString {
-        let font = UIFont.systemFont(ofSize: 12)
+        let font = UIFont.systemFont(ofSize: 12 + String.offset())
         return NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: font])
     }
     
@@ -3393,6 +3393,31 @@ public class MessageScope {
     public static let CHANNEL = "33";
 }
 
+class SecureField : UITextField {
+
+    override init(frame: CGRect) {
+        super.init(frame: .zero)
+        self.isSecureTextEntry = true
+        self.translatesAutoresizingMaskIntoConstraints = false
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    weak var secureContainer: UIView? {
+        let secureView = self.subviews.filter({ subview in
+            type(of: subview).description().contains("CanvasView")
+        }).first
+        secureView?.translatesAutoresizingMaskIntoConstraints = false
+        secureView?.isUserInteractionEnabled = true //To enable child view's userInteraction in iOS 13
+        return secureView
+    }
+    
+    override var canBecomeFirstResponder: Bool {false}
+    override func becomeFirstResponder() -> Bool {false}
+}
+
 class MediaViewerViewController: UIViewController, UIGestureRecognizerDelegate, UIScrollViewDelegate {
     
     enum MediaType {
@@ -3413,6 +3438,7 @@ class MediaViewerViewController: UIViewController, UIGestureRecognizerDelegate,
     private var playerLayer: AVPlayerLayer?
     private let playPauseButton = UIButton(type: .custom)
     private var isVideoPlaying = false
+    public var isSecure = false
 
     var isNavigationBarHidden = false {
         didSet { setNeedsStatusBarAppearanceUpdate() }
@@ -3421,11 +3447,23 @@ class MediaViewerViewController: UIViewController, UIGestureRecognizerDelegate,
     override var prefersStatusBarHidden: Bool {
         return isNavigationBarHidden
     }
+    
+    private var privacyOverlay: UIView = {
+        let view = UIView()
+        view.backgroundColor = .black
+        return view
+    }()
 
     override func viewDidLoad() {
         super.viewDidLoad()
 
         view.backgroundColor = .clear
+        
+        guard let secureView = SecureField().secureContainer else {return}
+        if isSecure {
+            setupPrivacyOverlay()
+            self.view.addSubview(secureView)
+        }
 
         edgesForExtendedLayout = .all
         extendedLayoutIncludesOpaqueBars = true
@@ -3439,7 +3477,11 @@ class MediaViewerViewController: UIViewController, UIGestureRecognizerDelegate,
         backgroundView.backgroundColor = .white
         backgroundView.alpha = 0
         backgroundView.frame = view.bounds
-        view.addSubview(backgroundView)
+        if isSecure {
+            secureView.addSubview(backgroundView)
+        } else {
+            view.addSubview(backgroundView)
+        }
 
         // ScrollView for zooming
         scrollView.frame = view.bounds
@@ -3449,7 +3491,11 @@ class MediaViewerViewController: UIViewController, UIGestureRecognizerDelegate,
         scrollView.showsVerticalScrollIndicator = false
         scrollView.showsHorizontalScrollIndicator = false
         scrollView.bouncesZoom = true
-        view.addSubview(scrollView)
+        if isSecure {
+            secureView.addSubview(scrollView)
+        } else {
+            view.addSubview(scrollView)
+        }
 
         // Add imageView to scrollView
         imageView.frame = scrollView.bounds
@@ -3477,6 +3523,51 @@ class MediaViewerViewController: UIViewController, UIGestureRecognizerDelegate,
         statusBarBackgroundView.autoresizingMask = [.flexibleWidth, .flexibleBottomMargin]
         view.addSubview(statusBarBackgroundView)
     }
+    
+    private func setupPrivacyOverlay() {
+        view.addSubview(privacyOverlay)
+        privacyOverlay.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            privacyOverlay.topAnchor.constraint(equalTo: view.topAnchor),
+            privacyOverlay.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+            privacyOverlay.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            privacyOverlay.trailingAnchor.constraint(equalTo: view.trailingAnchor)
+        ])
+
+        // Add WhatsApp-style message
+        let icon = UIImageView(image: UIImage(systemName: "camera.fill"))
+        icon.tintColor = .mainColor
+        icon.contentMode = .scaleAspectFit
+
+        let label = UILabel()
+        label.text = "Screen capture/recording blocked".localized()
+        label.font = .systemFont(ofSize: 22, weight: .semibold)
+        label.textColor = .white
+
+        let desc = UILabel()
+        desc.text = "You tried to take a screenshot.\nFor added privacy, credential messages don’t allow this.".localized()
+        desc.font = .systemFont(ofSize: 16)
+        desc.textColor = .lightGray
+        desc.numberOfLines = 0
+        desc.textAlignment = .center
+
+        let stack = UIStackView(arrangedSubviews: [icon, label, desc])
+        stack.axis = .vertical
+        stack.alignment = .center
+        stack.spacing = 18
+
+        privacyOverlay.addSubview(stack)
+        stack.translatesAutoresizingMaskIntoConstraints = false
+
+        NSLayoutConstraint.activate([
+            stack.centerXAnchor.constraint(equalTo: privacyOverlay.centerXAnchor),
+            stack.centerYAnchor.constraint(equalTo: privacyOverlay.centerYAnchor),
+            stack.leftAnchor.constraint(equalTo: view.leftAnchor),
+            stack.rightAnchor.constraint(equalTo: view.rightAnchor),
+            icon.widthAnchor.constraint(equalToConstant: 80),
+            icon.heightAnchor.constraint(equalToConstant: 80)
+        ])
+    }
 
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
@@ -3607,6 +3698,9 @@ class MediaViewerViewController: UIViewController, UIGestureRecognizerDelegate,
             let maxDistance = view.bounds.height / 2.0
             let progress = min(distance / maxDistance, 1.0)
             self.backgroundView.alpha = 1.0 - progress
+            if isSecure {
+                self.privacyOverlay.isHidden = true
+            }
 
         case .ended, .cancelled:
             let distance = hypot(translation.x, translation.y)
@@ -3617,6 +3711,9 @@ class MediaViewerViewController: UIViewController, UIGestureRecognizerDelegate,
                 dismiss(animated: true, completion: nil)
             } else {
                 // Return to center if not far enough
+                if isSecure {
+                    self.privacyOverlay.isHidden = false
+                }
                 UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.8, options: [], animations: {
                     self.scrollView.transform = .identity
                     self.backgroundView.alpha = 1.0

+ 150 - 30
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -999,9 +999,9 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     
     func updateProgress(_ data: [AnyHashable: Any]){
         var isImage = false
-        var idx = dataMessages.lastIndex(where: { $0["video_id"]  as? String ?? "" == data["name"]  as? String ?? "" || $0["video_id"] as? String == data["video_id"] as? String })
+        var idx = dataMessages.lastIndex(where: { $0["video_id"] as? String == data["name"] as? String || $0["video_id"] as? String == data["video_id"] as? String })
         if (idx == nil) {
-            idx = dataMessages.lastIndex(where: { $0["image_id"]  as? String ?? "" == data["name"]  as? String ?? "" || $0["image_id"] as? String == data["image_id"] as? String })
+            idx = dataMessages.lastIndex(where: { $0["image_id"] as? String == data["name"] as? String || $0["image_id"] as? String == data["image_id"] as? String })
             isImage = true
         }
         if (idx != nil) {
@@ -1009,7 +1009,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
             if section == nil {
                 return
             }
-            let row = dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[section!]}).firstIndex(where: { $0["message_id"]  as? String ?? "" == dataMessages[idx!]["message_id"]  as? String ?? ""})
+            let row = dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[section!]}).firstIndex(where: { $0["message_id"] as? String == dataMessages[idx!]["message_id"] as? String})
             if row == nil {
                 return
             }
@@ -1033,9 +1033,17 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                                     }
                                     var containerView : UIView?
                                     if (isImage) {
-                                        containerView = viewInContainer.subviews[0]
+                                        if viewInContainer.subviews[0] is UIVisualEffectView {
+                                            containerView = viewInContainer.subviews[1]
+                                        } else {
+                                            containerView = viewInContainer.subviews[0]
+                                        }
                                     } else if viewInContainer.subviews.count > 1 {
-                                        containerView = viewInContainer.subviews[1]
+                                        if viewInContainer.subviews[0] is UIVisualEffectView {
+                                            containerView = viewInContainer.subviews[2]
+                                        } else {
+                                            containerView = viewInContainer.subviews[1]
+                                        }
                                     }
                                     if let loading = containerView?.layer.sublayers?[1] as? CAShapeLayer {
                                         loading.strokeEnd = CGFloat(progress / 100)
@@ -1051,11 +1059,18 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                 }
             }
         } else {
-            idx = dataMessages.lastIndex(where: { $0["file_id"]  as? String ?? "" == data["name"]  as? String ?? "" || $0["file_id"] as? String == data["file_id"] as? String })
+            idx = dataMessages.lastIndex(where: { $0["file_id"] as? String == data["name"] as? String || $0["file_id"] as? String == data["file_id"] as? String })
             if (idx != nil) {
+                let section = dataDates.firstIndex(of: dataMessages[idx!]["chat_date"]  as? String ?? "")
+                if section == nil {
+                    return
+                }
+                let row = dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[section!]}).firstIndex(where: { $0["message_id"] as? String == dataMessages[idx!]["message_id"] as? String})
+                if row == nil {
+                    return
+                }
                 DispatchQueue.main.async {
-                    let section = 0
-                    let indexPath = IndexPath(row: idx!, section: section)
+                    let indexPath = IndexPath(row: row!, section: section!)
                     if(self.fakeProgMultip < self.maxFakeProgMultip){
                         self.fakeProgMultip = self.fakeProgMultip + 1
                     }
@@ -5977,11 +5992,16 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                 print("⚠️ modifyText: Invalid index \(indexPath.row), total: \(dataMessages.count)")
                 return
             }
+
             var text = textChat
             let messageData = dataMessages[indexPath.row]
+
+            // Remove segment after separator
             if let separatorRange = text.range(of: "■") {
                 text = String(text[..<separatorRange.lowerBound]).trimmingCharacters(in: .whitespacesAndNewlines)
             }
+
+            // Optional pipe-split logic
             if !fileChat.isEmpty {
                 let lock = messageData["lock"] as? String ?? ""
                 if lock != "1", lock != "2" {
@@ -5989,21 +6009,37 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                     if parts.count > 1 { text = parts[1] }
                 }
             }
-            let finalAttributed = text.richText()
+
+            // Must be mutable!
+            let finalAttributed = NSMutableAttributedString(attributedString: text.richText(group_id: self.dataGroup["group_id"]  as? String ?? ""))
+
             let urlPattern = "(https?://|www\\.)\\S+"
-            if let regex = try? NSRegularExpression(pattern: urlPattern, options: []) {
-                let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
-                for match in matches {
-                    guard let range = Range(match.range, in: text) else { continue }
-                    let linkText = String(text[range])
-                    let nsRange = NSRange(range, in: text)
-                    finalAttributed.addAttributes([
-                        .link: linkText,
-                        .foregroundColor: UIColor.systemBlue,
-                        .underlineStyle: NSUnderlineStyle.single.rawValue
-                    ], range: nsRange)
+            guard let regex = try? NSRegularExpression(pattern: urlPattern) else { return }
+
+            let fullString = finalAttributed.string
+            let fullLength = (fullString as NSString).length
+
+            let matches = regex.matches(in: fullString, range: NSRange(location: 0, length: fullLength))
+
+            for match in matches {
+                let range = match.range
+
+                // Skip invalid ranges safely
+                if range.location == NSNotFound ||
+                   range.location + range.length > fullLength ||
+                   range.length == 0 {
+                    continue
                 }
+
+                let linkText = (fullString as NSString).substring(with: range)
+
+                finalAttributed.addAttributes([
+                    .link: linkText,
+                    .foregroundColor: UIColor.systemBlue,
+                    .underlineStyle: NSUnderlineStyle.single.rawValue
+                ], range: range)
             }
+
             DispatchQueue.main.async {
                 messageText.attributedText = finalAttributed
                 messageText.delegate = self
@@ -6052,17 +6088,27 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
         
         if !audioChat.isEmpty {
             messageText.isHidden = true
+            var padTop: CGFloat = 32
+            if dataMessages[indexPath.row][TypeDataMessage.is_forwarded] != nil && dataMessages[indexPath.row][TypeDataMessage.is_forwarded] as! Int != 0 {
+                padTop = 52
+            }
+            
+            let contAudio = UIView()
+            contAudio.backgroundColor = .clear
+            containerMessage.addSubview(contAudio)
+            contAudio.anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, right: containerMessage.rightAnchor, paddingTop: padTop, paddingLeft: 15, paddingBottom: 15, paddingRight: 15)
+            
             let imageAudio = UIImageView()
             imageAudio.image = UIImage(systemName: "music.note", withConfiguration: UIImage.SymbolConfiguration(pointSize: 35))
-            containerMessage.addSubview(imageAudio)
-            imageAudio.anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, paddingTop: 15, paddingLeft: 15, paddingBottom: 15, centerY: containerMessage.centerYAnchor)
+            contAudio.addSubview(imageAudio)
+            imageAudio.anchor(top: contAudio.topAnchor, left: contAudio.leftAnchor, bottom: contAudio.bottomAnchor, centerY: contAudio.centerYAnchor)
             imageAudio.tintColor = .mainColor
             
             let playButtonAudio = UIButton(type: .system)
             playButtonAudio.setImage(UIImage(systemName: "play.fill"), for: .normal)
             playButtonAudio.tintColor = .gray
-            containerMessage.addSubview(playButtonAudio)
-            playButtonAudio.anchor(left: containerMessage.leftAnchor, paddingLeft: 60, centerY: containerMessage.centerYAnchor, width: 20, height: 20)
+            contAudio.addSubview(playButtonAudio)
+            playButtonAudio.anchor(left: contAudio.leftAnchor, paddingLeft: 45, centerY: contAudio.centerYAnchor, width: 20, height: 20)
             
             let progressSliderAudio = UISlider()
             progressSliderAudio.minimumValue = 0
@@ -6070,14 +6116,14 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
             let thumbImage = UIImage(systemName: "circle.fill")?.withTintColor(UIColor.mainColor)
                 .resize(target: CGSize(width: 15, height: 15))
             progressSliderAudio.setThumbImage(thumbImage, for: .normal)
-            containerMessage.addSubview(progressSliderAudio)
-            progressSliderAudio.anchor(left: playButtonAudio.rightAnchor, right: containerMessage.rightAnchor, paddingLeft: 10, paddingRight: 15, centerY: containerMessage.centerYAnchor, height: 15)
+            contAudio.addSubview(progressSliderAudio)
+            progressSliderAudio.anchor(left: playButtonAudio.rightAnchor, right: contAudio.rightAnchor, paddingLeft: 10, centerY: contAudio.centerYAnchor, height: 15)
             
             let timeLabelAudio = UILabel()
             timeLabelAudio.text = "0:00"
             timeLabelAudio.font = .systemFont(ofSize: 10 + offset())
             timeLabelAudio.textColor = .gray
-            containerMessage.addSubview(timeLabelAudio)
+            contAudio.addSubview(timeLabelAudio)
             timeLabelAudio.anchor(top: playButtonAudio.bottomAnchor, left: playButtonAudio.rightAnchor, paddingLeft: 10, width: 100, height: 12)
             
             let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
@@ -6236,6 +6282,12 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                             blurEffectView.frame = CGRect(x: 0, y: 0, width: listImageThumb[i].frame.size.width, height: listImageThumb[i].frame.size.height)
                             blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
                             listImageThumb[i].addSubview(blurEffectView)
+                        } else if (dataMessages[indexPath.row]["credential"] as? String) == "1" && (dataMessages[indexPath.row]["lock"] as? String) != "2" && (dataMessages[indexPath.row]["lock"] as? String) != "1" {
+                            let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.dark)
+                            let blurEffectView = UIVisualEffectView(effect: blurEffect)
+                            blurEffectView.frame = CGRect(x: 0, y: 0, width: imageThumb.frame.size.width, height: imageThumb.frame.size.height)
+                            blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+                            listImageThumb[i].addSubview(blurEffectView)
                         }
                         
                     }
@@ -6399,6 +6451,12 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                             imageDownload.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
                             imageDownload.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
                         }
+                    } else if (dataMessages[indexPath.row]["credential"] as? String) == "1" && (dataMessages[indexPath.row]["lock"] as? String) != "2" && (dataMessages[indexPath.row]["lock"] as? String) != "1" {
+                        let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.dark)
+                        let blurEffectView = UIVisualEffectView(effect: blurEffect)
+                        blurEffectView.frame = CGRect(x: 0, y: 0, width: imageThumb.frame.size.width, height: imageThumb.frame.size.height)
+                        blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+                        imageThumb.addSubview(blurEffectView)
                     }
                     
                 }
@@ -7360,6 +7418,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                 formatter.dateFormat = "dd/MM/yy HH:mm"
                 imageViewer.subtitleCustom = formatter.string(from: date)
             }
+            imageViewer.isSecure = dataMessages[indexPath.row][TypeDataMessage.credential] as? String == "1"
             
             let transitionDelegate = ZoomTransitioningDelegate()
             transitionDelegate.originImageView = sender.imageView
@@ -7553,12 +7612,73 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                     vcHandleFile.navigationItem.rightBarButtonItem = shareButton
                 }
                 if let viewVc = vcHandleFile.view {
+                    let isSecure = dataMessages[indexPath.row][TypeDataMessage.credential] as? String == "1"
                     vcHandleFile.title = sender.labelFile.text
+                    var secureView: UIView!
+                    if isSecure {
+                        secureView = SecureField().secureContainer
+                        
+                        let privacyOverlay: UIView = {
+                            let view = UIView()
+                            view.backgroundColor = .black
+                            return view
+                        }()
+                        
+                        viewVc.addSubview(privacyOverlay)
+                        privacyOverlay.translatesAutoresizingMaskIntoConstraints = false
+                        NSLayoutConstraint.activate([
+                            privacyOverlay.topAnchor.constraint(equalTo: view.topAnchor),
+                            privacyOverlay.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+                            privacyOverlay.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+                            privacyOverlay.trailingAnchor.constraint(equalTo: view.trailingAnchor)
+                        ])
+
+                        // Add WhatsApp-style message
+                        let icon = UIImageView(image: UIImage(systemName: "camera.fill"))
+                        icon.tintColor = .mainColor
+                        icon.contentMode = .scaleAspectFit
+
+                        let label = UILabel()
+                        label.text = "Screen capture/recording blocked".localized()
+                        label.font = .systemFont(ofSize: 22, weight: .semibold)
+                        label.textColor = .white
+
+                        let desc = UILabel()
+                        desc.text = "You tried to take a screenshot.\nFor added privacy, credential messages don’t allow this.".localized()
+                        desc.font = .systemFont(ofSize: 16)
+                        desc.textColor = .lightGray
+                        desc.numberOfLines = 0
+                        desc.textAlignment = .center
+
+                        let stack = UIStackView(arrangedSubviews: [icon, label, desc])
+                        stack.axis = .vertical
+                        stack.alignment = .center
+                        stack.spacing = 18
+
+                        privacyOverlay.addSubview(stack)
+                        stack.translatesAutoresizingMaskIntoConstraints = false
+
+                        NSLayoutConstraint.activate([
+                            stack.centerXAnchor.constraint(equalTo: privacyOverlay.centerXAnchor),
+                            stack.centerYAnchor.constraint(equalTo: privacyOverlay.centerYAnchor),
+                            stack.leftAnchor.constraint(equalTo: view.leftAnchor),
+                            stack.rightAnchor.constraint(equalTo: view.rightAnchor),
+                            icon.widthAnchor.constraint(equalToConstant: 80),
+                            icon.heightAnchor.constraint(equalToConstant: 80)
+                        ])
+                        
+                        viewVc.addSubview(secureView)
+                        secureView.frame = CGRect(x: 0, y: 0, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height)
+                    }
                     vcHandleFile.addChild(previewController)
                     previewController.dataSource = self
-                    previewController.view.frame = CGRect(x: 0, y: 0, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height)
+                    previewController.view.frame = CGRect(x: 0, y: 0, width: isSecure ? secureView.bounds.size.width : viewVc.bounds.size.width, height: isSecure ? secureView.bounds.size.height : viewVc.bounds.size.height)
                     previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
-                    viewVc.addSubview(previewController.view)
+                    if isSecure {
+                        secureView.addSubview(previewController.view)
+                    } else {
+                        viewVc.addSubview(previewController.view)
+                    }
                     previewController.didMove(toParent: vcHandleFile)
                     
                     self.present(nc, animated: true)
@@ -8217,7 +8337,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
 //            }
 //        }
 //    }
-//    
+//
 //    public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
 //        if !decelerate {
 //            let indexPath = tableChatView.indexPathsForVisibleRows?.first

+ 137 - 26
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -1458,9 +1458,17 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                                     }
                                     var containerView : UIView?
                                     if (isImage) {
-                                        containerView = viewInContainer.subviews[0]
+                                        if viewInContainer.subviews[0] is UIVisualEffectView {
+                                            containerView = viewInContainer.subviews[1]
+                                        } else {
+                                            containerView = viewInContainer.subviews[0]
+                                        }
                                     } else if viewInContainer.subviews.count > 1 {
-                                        containerView = viewInContainer.subviews[1]
+                                        if viewInContainer.subviews[0] is UIVisualEffectView {
+                                            containerView = viewInContainer.subviews[2]
+                                        } else {
+                                            containerView = viewInContainer.subviews[1]
+                                        }
                                     }
                                     if let loading = containerView?.layer.sublayers?[1] as? CAShapeLayer {
                                         loading.strokeEnd = CGFloat(progress / 100)
@@ -7688,11 +7696,16 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 print("⚠️ modifyText: Invalid index \(indexPath.row), total: \(dataMessages.count)")
                 return
             }
+
             var text = textChat
             let messageData = dataMessages[indexPath.row]
+
+            // Remove segment after separator
             if let separatorRange = text.range(of: "■") {
                 text = String(text[..<separatorRange.lowerBound]).trimmingCharacters(in: .whitespacesAndNewlines)
             }
+
+            // Optional pipe-split logic
             if !fileChat.isEmpty {
                 let lock = messageData["lock"] as? String ?? ""
                 if lock != "1", lock != "2" {
@@ -7700,21 +7713,37 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                     if parts.count > 1 { text = parts[1] }
                 }
             }
-            let finalAttributed = text.richText()
+
+            // Must be mutable!
+            let finalAttributed = NSMutableAttributedString(attributedString: text.richText())
+
             let urlPattern = "(https?://|www\\.)\\S+"
-            if let regex = try? NSRegularExpression(pattern: urlPattern, options: []) {
-                let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
-                for match in matches {
-                    guard let range = Range(match.range, in: text) else { continue }
-                    let linkText = String(text[range])
-                    let nsRange = NSRange(range, in: text)
-                    finalAttributed.addAttributes([
-                        .link: linkText,
-                        .foregroundColor: UIColor.systemBlue,
-                        .underlineStyle: NSUnderlineStyle.single.rawValue
-                    ], range: nsRange)
+            guard let regex = try? NSRegularExpression(pattern: urlPattern) else { return }
+
+            let fullString = finalAttributed.string
+            let fullLength = (fullString as NSString).length
+
+            let matches = regex.matches(in: fullString, range: NSRange(location: 0, length: fullLength))
+
+            for match in matches {
+                let range = match.range
+
+                // Skip invalid ranges safely
+                if range.location == NSNotFound ||
+                   range.location + range.length > fullLength ||
+                   range.length == 0 {
+                    continue
                 }
+
+                let linkText = (fullString as NSString).substring(with: range)
+
+                finalAttributed.addAttributes([
+                    .link: linkText,
+                    .foregroundColor: UIColor.systemBlue,
+                    .underlineStyle: NSUnderlineStyle.single.rawValue
+                ], range: range)
             }
+
             DispatchQueue.main.async {
                 messageText.attributedText = finalAttributed
                 messageText.delegate = self
@@ -7843,17 +7872,27 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
         
         if !audioChat.isEmpty {
             messageText.isHidden = true
+            var padTop: CGFloat = 15
+            if dataMessages[indexPath.row][TypeDataMessage.is_forwarded] != nil && dataMessages[indexPath.row][TypeDataMessage.is_forwarded] as! Int != 0 {
+                padTop = 35
+            }
+            
+            let contAudio = UIView()
+            contAudio.backgroundColor = .clear
+            containerMessage.addSubview(contAudio)
+            contAudio.anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, right: containerMessage.rightAnchor, paddingTop: padTop, paddingLeft: 15, paddingBottom: 15, paddingRight: 15)
+            
             let imageAudio = UIImageView()
             imageAudio.image = UIImage(systemName: "music.note", withConfiguration: UIImage.SymbolConfiguration(pointSize: 35))
-            containerMessage.addSubview(imageAudio)
-            imageAudio.anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, paddingTop: 15, paddingLeft: 15, paddingBottom: 15, centerY: containerMessage.centerYAnchor)
+            contAudio.addSubview(imageAudio)
+            imageAudio.anchor(top: contAudio.topAnchor, left: contAudio.leftAnchor, bottom: contAudio.bottomAnchor, centerY: contAudio.centerYAnchor)
             imageAudio.tintColor = .mainColor
             
             let playButtonAudio = UIButton(type: .system)
             playButtonAudio.setImage(UIImage(systemName: "play.fill"), for: .normal)
             playButtonAudio.tintColor = .gray
-            containerMessage.addSubview(playButtonAudio)
-            playButtonAudio.anchor(left: containerMessage.leftAnchor, paddingLeft: 60, centerY: containerMessage.centerYAnchor, width: 20, height: 20)
+            contAudio.addSubview(playButtonAudio)
+            playButtonAudio.anchor(left: contAudio.leftAnchor, paddingLeft: 45, centerY: contAudio.centerYAnchor, width: 20, height: 20)
             
             let progressSliderAudio = UISlider()
             progressSliderAudio.minimumValue = 0
@@ -7861,14 +7900,14 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
             let thumbImage = UIImage(systemName: "circle.fill")?.withTintColor(UIColor.mainColor)
                 .resize(target: CGSize(width: 15, height: 15))
             progressSliderAudio.setThumbImage(thumbImage, for: .normal)
-            containerMessage.addSubview(progressSliderAudio)
-            progressSliderAudio.anchor(left: playButtonAudio.rightAnchor, right: containerMessage.rightAnchor, paddingLeft: 10, paddingRight: 15, centerY: containerMessage.centerYAnchor, height: 15)
+            contAudio.addSubview(progressSliderAudio)
+            progressSliderAudio.anchor(left: playButtonAudio.rightAnchor, right: contAudio.rightAnchor, paddingLeft: 10, centerY: contAudio.centerYAnchor, height: 15)
             
             let timeLabelAudio = UILabel()
             timeLabelAudio.text = "0:00"
             timeLabelAudio.font = .systemFont(ofSize: 10 + offset())
             timeLabelAudio.textColor = .gray
-            containerMessage.addSubview(timeLabelAudio)
+            contAudio.addSubview(timeLabelAudio)
             timeLabelAudio.anchor(top: playButtonAudio.bottomAnchor, left: playButtonAudio.rightAnchor, paddingLeft: 10, width: 100, height: 12)
             
             let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
@@ -8027,6 +8066,12 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                             blurEffectView.frame = CGRect(x: 0, y: 0, width: listImageThumb[i].frame.size.width, height: listImageThumb[i].frame.size.height)
                             blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
                             listImageThumb[i].addSubview(blurEffectView)
+                        } else if (dataMessages[indexPath.row]["credential"] as? String) == "1" && (dataMessages[indexPath.row]["lock"] as? String) != "2" && (dataMessages[indexPath.row]["lock"] as? String) != "1" {
+                            let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.dark)
+                            let blurEffectView = UIVisualEffectView(effect: blurEffect)
+                            blurEffectView.frame = CGRect(x: 0, y: 0, width: imageThumb.frame.size.width, height: imageThumb.frame.size.height)
+                            blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+                            listImageThumb[i].addSubview(blurEffectView)
                         }
 
                     }
@@ -8188,6 +8233,12 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                             imageDownload.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
                             imageDownload.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
                         }
+                    } else if (dataMessages[indexPath.row]["credential"] as? String) == "1" && (dataMessages[indexPath.row]["lock"] as? String) != "2" && (dataMessages[indexPath.row]["lock"] as? String) != "1" {
+                        let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.dark)
+                        let blurEffectView = UIVisualEffectView(effect: blurEffect)
+                        blurEffectView.frame = CGRect(x: 0, y: 0, width: imageThumb.frame.size.width, height: imageThumb.frame.size.height)
+                        blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+                        imageThumb.addSubview(blurEffectView)
                     }
                     
                 }
@@ -9186,6 +9237,7 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 formatter.dateFormat = "dd/MM/yy HH:mm"
                 imageViewer.subtitleCustom = formatter.string(from: date)
             }
+            imageViewer.isSecure = dataMessages[indexPath.row][TypeDataMessage.credential] as? String == "1"
             
             let transitionDelegate = ZoomTransitioningDelegate()
             transitionDelegate.originImageView = sender.imageView
@@ -9384,14 +9436,73 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                     vcHandleFile.navigationItem.rightBarButtonItem = shareButton
                 }
                 if let viewVc = vcHandleFile.view {
-                    if isFile {
-                        vcHandleFile.title = sender.labelFile.text
+                    let isSecure = dataMessages[indexPath.row][TypeDataMessage.credential] as? String == "1"
+                    vcHandleFile.title = sender.labelFile.text
+                    var secureView: UIView!
+                    if isSecure {
+                        secureView = SecureField().secureContainer
+                        
+                        let privacyOverlay: UIView = {
+                            let view = UIView()
+                            view.backgroundColor = .black
+                            return view
+                        }()
+                        
+                        viewVc.addSubview(privacyOverlay)
+                        privacyOverlay.translatesAutoresizingMaskIntoConstraints = false
+                        NSLayoutConstraint.activate([
+                            privacyOverlay.topAnchor.constraint(equalTo: view.topAnchor),
+                            privacyOverlay.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+                            privacyOverlay.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+                            privacyOverlay.trailingAnchor.constraint(equalTo: view.trailingAnchor)
+                        ])
+
+                        // Add WhatsApp-style message
+                        let icon = UIImageView(image: UIImage(systemName: "camera.fill"))
+                        icon.tintColor = .mainColor
+                        icon.contentMode = .scaleAspectFit
+
+                        let label = UILabel()
+                        label.text = "Screen capture/recording blocked".localized()
+                        label.font = .systemFont(ofSize: 22, weight: .semibold)
+                        label.textColor = .white
+
+                        let desc = UILabel()
+                        desc.text = "You tried to take a screenshot.\nFor added privacy, credential messages don’t allow this.".localized()
+                        desc.font = .systemFont(ofSize: 16)
+                        desc.textColor = .lightGray
+                        desc.numberOfLines = 0
+                        desc.textAlignment = .center
+
+                        let stack = UIStackView(arrangedSubviews: [icon, label, desc])
+                        stack.axis = .vertical
+                        stack.alignment = .center
+                        stack.spacing = 18
+
+                        privacyOverlay.addSubview(stack)
+                        stack.translatesAutoresizingMaskIntoConstraints = false
+
+                        NSLayoutConstraint.activate([
+                            stack.centerXAnchor.constraint(equalTo: privacyOverlay.centerXAnchor),
+                            stack.centerYAnchor.constraint(equalTo: privacyOverlay.centerYAnchor),
+                            stack.leftAnchor.constraint(equalTo: view.leftAnchor),
+                            stack.rightAnchor.constraint(equalTo: view.rightAnchor),
+                            icon.widthAnchor.constraint(equalToConstant: 80),
+                            icon.heightAnchor.constraint(equalToConstant: 80)
+                        ])
+                        
+                        viewVc.addSubview(secureView)
+                        secureView.frame = CGRect(x: 0, y: 0, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height)
                     }
                     vcHandleFile.addChild(previewController)
                     previewController.dataSource = self
-                    previewController.view.frame = CGRect(x: 0, y: 0, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height)
+                    previewController.view.frame = CGRect(x: 0, y: 0, width: isSecure ? secureView.bounds.size.width : viewVc.bounds.size.width, height: isSecure ? secureView.bounds.size.height : viewVc.bounds.size.height)
                     previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
-                    viewVc.addSubview(previewController.view)
+                    if isSecure {
+                        secureView.addSubview(previewController.view)
+                    } else {
+                        viewVc.addSubview(previewController.view)
+                    }
                     previewController.didMove(toParent: vcHandleFile)
                     
                     self.present(nc, animated: true)
@@ -10083,7 +10194,7 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
 //            }
 //        }
 //    }
-//    
+//
 //    public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
 //        if !decelerate {
 //            let indexPath = tableChatView.indexPathsForVisibleRows?.first

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

@@ -484,11 +484,16 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                 print("⚠️ modifyText: Invalid index \(indexPath.row), total: \(dataMessages.count)")
                 return
             }
+
             var text = textChat
             let messageData = dataMessages[indexPath.row]
+
+            // Remove segment after separator
             if let separatorRange = text.range(of: "■") {
                 text = String(text[..<separatorRange.lowerBound]).trimmingCharacters(in: .whitespacesAndNewlines)
             }
+
+            // Optional pipe-split logic
             if !fileChat.isEmpty {
                 let lock = messageData["lock"] as? String ?? ""
                 if lock != "1", lock != "2" {
@@ -496,21 +501,37 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                     if parts.count > 1 { text = parts[1] }
                 }
             }
-            let finalAttributed = text.richText()
+
+            // Must be mutable!
+            let finalAttributed = NSMutableAttributedString(attributedString: text.richText())
+
             let urlPattern = "(https?://|www\\.)\\S+"
-            if let regex = try? NSRegularExpression(pattern: urlPattern, options: []) {
-                let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
-                for match in matches {
-                    guard let range = Range(match.range, in: text) else { continue }
-                    let linkText = String(text[range])
-                    let nsRange = NSRange(range, in: text)
-                    finalAttributed.addAttributes([
-                        .link: linkText,
-                        .foregroundColor: UIColor.systemBlue,
-                        .underlineStyle: NSUnderlineStyle.single.rawValue
-                    ], range: nsRange)
+            guard let regex = try? NSRegularExpression(pattern: urlPattern) else { return }
+
+            let fullString = finalAttributed.string
+            let fullLength = (fullString as NSString).length
+
+            let matches = regex.matches(in: fullString, range: NSRange(location: 0, length: fullLength))
+
+            for match in matches {
+                let range = match.range
+
+                // Skip invalid ranges safely
+                if range.location == NSNotFound ||
+                   range.location + range.length > fullLength ||
+                   range.length == 0 {
+                    continue
                 }
+
+                let linkText = (fullString as NSString).substring(with: range)
+
+                finalAttributed.addAttributes([
+                    .link: linkText,
+                    .foregroundColor: UIColor.systemBlue,
+                    .underlineStyle: NSUnderlineStyle.single.rawValue
+                ], range: range)
             }
+
             DispatchQueue.main.async {
                 messageText.attributedText = finalAttributed
                 messageText.delegate = self
@@ -1868,6 +1889,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
     }
     
     public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        let dataMessages = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[indexPath.section]})
         let message = dataMessages[indexPath.row]
         if let attachmentFlag = message["attachment_flag"], let attachmentFlag = attachmentFlag as? String {
             if attachmentFlag == "27" {

+ 10 - 4
NexilisLite/NexilisLite/Source/View/Chat/MessageInfo.swift

@@ -445,7 +445,7 @@ class MessageInfo: UIViewController, UITableViewDelegate, UITableViewDataSource,
                         content.text = "Confirmed".localized() + " at (\(dataLocation[0]))"
                     }
                     if dataStatus.count != 0 {
-                        if (dataStatus[0]["time_ack"] as! String).isEmpty {
+                        if (dataStatus[0]["time_ack"] as? String ?? "").isEmpty {
                             cell.accessoryView = noStatus
                         } else {
                             let date = Date(milliseconds: Int64(dataStatus[0]["time_ack"] as! String) ?? 100)
@@ -465,12 +465,14 @@ class MessageInfo: UIViewController, UITableViewDelegate, UITableViewDataSource,
                             
                             cell.accessoryView = viewTimeStatus
                         }
+                    } else {
+                        cell.accessoryView = noStatus
                     }
                 } else {
                     content.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.systemBlue)
                     content.text = "Read".localized()
                     if dataStatus.count != 0 {
-                        if (dataStatus[0]["time_read"] as! String).isEmpty {
+                        if (dataStatus[0]["time_read"] as? String ?? "").isEmpty {
                             cell.accessoryView = noStatus
                         } else {
                             let date = Date(milliseconds: Int64(dataStatus[0]["time_read"] as! String) ?? 100)
@@ -490,13 +492,15 @@ class MessageInfo: UIViewController, UITableViewDelegate, UITableViewDataSource,
                             
                             cell.accessoryView = viewTimeStatus
                         }
+                    } else {
+                        cell.accessoryView = noStatus
                     }
                 }
             } else if indexPath.row == 2 && !data.isEmpty && data["read_receipts"] as? String == "8" {
                 content.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.systemBlue)
                 content.text = "Read".localized()
                 if dataStatus.count != 0 {
-                    if (dataStatus[0]["time_read"] as! String).isEmpty {
+                    if (dataStatus[0]["time_read"] as? String ?? "").isEmpty {
                         cell.accessoryView = noStatus
                     } else {
                         let date = Date(milliseconds: Int64(dataStatus[0]["time_read"] as! String) ?? 100)
@@ -516,11 +520,13 @@ class MessageInfo: UIViewController, UITableViewDelegate, UITableViewDataSource,
                         
                         cell.accessoryView = viewTimeStatus
                     }
+                } else {
+                    cell.accessoryView = noStatus
                 }
             } else {
                 content.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.lightGray)
                 content.text = "Delivered".localized()
-                if (dataStatus[0]["time_delivered"] as! String).isEmpty {
+                if dataStatus.count == 0 || (dataStatus[0]["time_delivered"] as? String ?? "").isEmpty {
                     cell.accessoryView = noStatus
                 } else {
                     let date = Date(milliseconds: Int64(dataStatus[0]["time_delivered"] as! String) ?? 100)