Bläddra i källkod

update and release for 5.0.74

alqindiirsyam 6 dagar sedan
förälder
incheckning
5fbf7604ab

BIN
.DS_Store


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

@@ -584,7 +584,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.73;
+				MARKETING_VERSION = 5.0.74;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -620,7 +620,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.73;
+				MARKETING_VERSION = 5.0.74;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -656,7 +656,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.73;
+				MARKETING_VERSION = 5.0.74;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -695,7 +695,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.73;
+				MARKETING_VERSION = 5.0.74;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 58 - 38
AppBuilder/AppBuilder/SecondTabViewController.swift

@@ -812,15 +812,17 @@ class SecondTabViewController: UIViewController, UIScrollViewDelegate, UIGesture
     private func reloadAllData() {
 //        print("reloadAllData")
         DispatchQueue.main.async { [weak self] in
-            if self?.timerReloadData == nil && !self!.isGettingData {
-                self?.getData()
+            guard let self = self else { return }
+            if self.timerReloadData == nil && !self.isGettingData {
+                self.getData()
             } else {
-                self?.timerReloadData?.invalidate()
-                self?.timerReloadData = nil
-                self?.timerReloadData = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
+                self.timerReloadData?.invalidate()
+                self.timerReloadData = nil
+                self.timerReloadData = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
                     if self != nil && !self!.isGettingData {
-                        self?.getData()
-                        self?.timerReloadData = nil
+                        guard let self = self else { return }
+                        self.getData()
+                        self.timerReloadData = nil
                     }
                 }
             }
@@ -2565,7 +2567,7 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
         let imgData = data.image
         let vidData = data.video
         let gifData = data.gif
-        let image = UIImageView()
+        let imageView = UIImageView()
         let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
         let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
         let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
@@ -2611,52 +2613,70 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                 }
                 return cell
             }
-            cell.contentView.addSubview(image)
-            let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumb)
-            let imageT : UIImage? =  {
-                if let img = Nexilis.imageCache.object(forKey: thumb as NSString) {
-                    return img
+            if FileEncryption.shared.isSecureExists(filename: thumb) {
+                do {
+                    if var data = try FileEncryption.shared.readSecure(filename: thumb) {
+                        let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                        if dataDecrypt != nil {
+                            data = dataDecrypt!
+                        }
+                        DispatchQueue.main.async {
+                            let image : UIImage? =  {
+                                if let img = Nexilis.imageCache.object(forKey: thumb as NSString) {
+                                    return img
+                                }
+                                else if let img = UIImage(data: data)?.resize(target: CGSize(width: 500, height: 500)) {
+                                    Nexilis.imageCache.setObject(img, forKey: thumb as NSString)
+                                    return img
+                                }
+                                return nil
+                            }()
+                            imageView.image = image
+                        }
+                    }
+                } catch {
+                    
                 }
-                else if let img = UIImage(contentsOfFile: thumbURL.path)?.resize(target: CGSize(width: 500, height: 500)) {
-                    Nexilis.imageCache.setObject(img, forKey: thumb as NSString)
-                    return img
+            } else {
+                Download().startHTTP(forKey: thumb) { (name, progress) in
+                    guard progress == 100 else {
+                        return
+                    }
+                    collectionView.reloadItems(at: [indexPath])
                 }
-                return nil
-            }()
-            if imageT != nil {
-                image.image = imageT
             }
+            cell.contentView.addSubview(imageView)
             
             let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(imgData)
             let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(vidData)
             if (!FileManager.default.fileExists(atPath: imageURL.path) && !FileEncryption.shared.isSecureExists(filename: imageURL.lastPathComponent)) || (!FileManager.default.fileExists(atPath: videoURL.path) && !FileEncryption.shared.isSecureExists(filename: videoURL.lastPathComponent)) {
                 let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.light)
                 let blurEffectView = UIVisualEffectView(effect: blurEffect)
-                blurEffectView.frame = CGRect(x: 0, y: 0, width: image.frame.size.width, height: image.frame.size.height)
+                blurEffectView.frame = CGRect(x: 0, y: 0, width: imageView.frame.size.width, height: imageView.frame.size.height)
                 blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
-                image.addSubview(blurEffectView)
+                imageView.addSubview(blurEffectView)
                 if !imgData.isEmpty {
                     let imageDownload = UIImageView(image: UIImage(systemName: "arrow.down.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 50, weight: .bold, scale: .default)))
-                    image.addSubview(imageDownload)
+                    imageView.addSubview(imageDownload)
                     imageDownload.tintColor = .black.withAlphaComponent(0.3)
                     imageDownload.translatesAutoresizingMaskIntoConstraints = false
-                    imageDownload.centerXAnchor.constraint(equalTo: image.centerXAnchor).isActive = true
-                    imageDownload.centerYAnchor.constraint(equalTo: image.centerYAnchor).isActive = true
+                    imageDownload.centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true
+                    imageDownload.centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true
                 }
             }
             if (!vidData.isEmpty) {
                 let imagePlay = UIImageView(image: UIImage(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .bold, scale: .default))?.imageWithInsets(insets: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10))?.withTintColor(.white))
                 imagePlay.circle()
-                image.addSubview(imagePlay)
+                imageView.addSubview(imagePlay)
                 imagePlay.backgroundColor = .black.withAlphaComponent(0.3)
                 imagePlay.translatesAutoresizingMaskIntoConstraints = false
-                imagePlay.centerXAnchor.constraint(equalTo: image.centerXAnchor).isActive = true
-                imagePlay.centerYAnchor.constraint(equalTo: image.centerYAnchor).isActive = true
+                imagePlay.centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true
+                imagePlay.centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true
             }
         }
-        image.contentMode = .scaleAspectFill
-        image.clipsToBounds = true
-        image.anchor(top: cell.contentView.topAnchor, left: cell.contentView.leftAnchor, bottom: cell.contentView.bottomAnchor, right: cell.contentView.rightAnchor)
+        imageView.contentMode = .scaleAspectFill
+        imageView.clipsToBounds = true
+        imageView.anchor(top: cell.contentView.topAnchor, left: cell.contentView.leftAnchor, bottom: cell.contentView.bottomAnchor, right: cell.contentView.rightAnchor)
         return cell
     }
     
@@ -2674,18 +2694,18 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                 let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(imgData)
                 if FileManager.default.fileExists(atPath: imageURL.path) {
                     do {
-                        APIS.openImageNexilis(imageView: (cell.contentView.subviews[0] as? UIImageView) ?? UIImageView(), data: try Data(contentsOf: imageURL))
+                        APIS.openImageNexilis(imageView: (cell.contentView.subviews[0] as? UIImageView) ?? UIImageView(), data: try Data(contentsOf: imageURL), nameSender: data.name, time: data.serverDate)
                     } catch {
                         
                     }
                 } else if FileEncryption.shared.isSecureExists(filename: imgData) {
                     do {
-                        if var data = try FileEncryption.shared.readSecure(filename: imgData) {
-                            let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
+                        if var dataImage = try FileEncryption.shared.readSecure(filename: imgData) {
+                            let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: dataImage)
                             if dataDecrypt != nil {
-                                data = dataDecrypt!
+                                dataImage = dataDecrypt!
                             }
-                            APIS.openImageNexilis(imageView: (cell.contentView.subviews[0] as? UIImageView) ?? UIImageView(), data: data)
+                            APIS.openImageNexilis(imageView: (cell.contentView.subviews[0] as? UIImageView) ?? UIImageView(), data: dataImage, nameSender: data.name, time: data.serverDate)
                         }
                     }
                     catch {
@@ -2703,7 +2723,7 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
             } else if selectedTag == VIDEOS_TAG {
                 let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(vidData)
                 if FileManager.default.fileExists(atPath: videoURL.path) {
-                    APIS.openVideoNexilis(videoURL: videoURL)
+                    APIS.openVideoNexilis(imageView: (cell.contentView.subviews[0] as? UIImageView) ?? UIImageView(), videoURL: videoURL, nameSender: data.name, time: data.serverDate)
                 } else if FileEncryption.shared.isSecureExists(filename: vidData) {
                     do {
                         if var secureData = try FileEncryption.shared.readSecure(filename: vidData) {
@@ -2714,7 +2734,7 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                             let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
                             let tempPath = cachesDirectory.appendingPathComponent(vidData)
                             try secureData.write(to: tempPath)
-                            APIS.openVideoNexilis(videoURL: tempPath)
+                            APIS.openVideoNexilis(imageView: (cell.contentView.subviews[0] as? UIImageView) ?? UIImageView(), videoURL: videoURL, nameSender: data.name, time: data.serverDate)
                         }
                     } catch {
                         

+ 45 - 11
NexilisLite/NexilisLite/Source/APIS.swift

@@ -2993,7 +2993,7 @@ public class APIS: NSObject {
         nameGroupShared = name
     }
     
-    public static func openImageNexilis(imageView: UIImageView, data: Data? = nil, isGIF: Bool = false) {
+    public static func openImageNexilis(imageView: UIImageView, data: Data? = nil, isGIF: Bool = false, nameSender: String = "", time: String = "") {
         let image = UIImage(data: data ?? Data())
         let imageViewer = MediaViewerViewController()
         if !isGIF {
@@ -3013,9 +3013,15 @@ public class APIS: NSObject {
         }
         let backButton = UIBarButtonItem(title: nil, image: UIImage(systemName: "chevron.backward"), primaryAction: backAction, menu: nil)
         imageViewer.navigationItem.leftBarButtonItem = backButton
-        
-        let name = ""
-        imageViewer.title = name
+        imageViewer.titleCustom = nameSender
+        if !time.isEmpty {
+            if let timestamp = Double(time) {
+                let date = Date(timeIntervalSince1970: timestamp / 1000)
+                let formatter = DateFormatter()
+                formatter.dateFormat = "dd/MM/yy HH:mm"
+                imageViewer.subtitleCustom = formatter.string(from: date)
+            }
+        }
         
         let transitionDelegate = ZoomTransitioningDelegate()
         transitionDelegate.originImageView = imageView
@@ -3033,15 +3039,43 @@ public class APIS: NSObject {
         }
     }
     
-    public static func openVideoNexilis(videoURL: URL) {
-        let player = AVPlayer(url: videoURL)
-        let playerVC = AVPlayerViewController()
-        playerVC.modalPresentationStyle = .custom
-        playerVC.player = player
+    public static func openVideoNexilis(imageView: UIImageView, videoURL: URL, nameSender: String = "", time: String = "") {
+        let imageViewer = MediaViewerViewController()
+        imageViewer.media = .video(videoURL)
+        let navigationController = UINavigationController(rootViewController: imageViewer)
+        navigationController.defaultStyle()
+        navigationController.view.backgroundColor = .clear
+        navigationController.modalPresentationCapturesStatusBarAppearance = true
+        navigationController.modalPresentationStyle = .overFullScreen
+        
+        let backAction = UIAction { _ in
+            navigationController.dismiss(animated: true)
+        }
+        let backButton = UIBarButtonItem(title: nil, image: UIImage(systemName: "chevron.backward"), primaryAction: backAction, menu: nil)
+        imageViewer.navigationItem.leftBarButtonItem = backButton
+        imageViewer.titleCustom = nameSender
+        if !time.isEmpty {
+            if let timestamp = Double(time) {
+                let date = Date(timeIntervalSince1970: timestamp / 1000)
+                let formatter = DateFormatter()
+                formatter.dateFormat = "dd/MM/yy HH:mm"
+                imageViewer.subtitleCustom = formatter.string(from: date)
+            }
+        }
+        
+        let transitionDelegate = ZoomTransitioningDelegate()
+        transitionDelegate.originImageView = imageView
+        navigationController.transitioningDelegate = transitionDelegate
+        self.transitioningDelegateRef = transitionDelegate
+        
         if UIApplication.shared.visibleViewController?.navigationController != nil {
-            UIApplication.shared.visibleViewController?.navigationController?.present(playerVC, animated: true, completion: nil)
+            UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true) {
+                imageViewer.animateBackgroundIn()
+            }
         } else {
-            UIApplication.shared.visibleViewController?.present(playerVC, animated: true, completion: nil)
+            UIApplication.shared.visibleViewController?.present(navigationController, animated: true) {
+                imageViewer.animateBackgroundIn()
+            }
         }
     }
     

+ 6 - 3
NexilisLite/NexilisLite/Source/Extension.swift

@@ -1004,9 +1004,12 @@ extension String {
     }
     
     public func substring(with nsRange: NSRange) -> String? {
-        guard let range = Range(nsRange, in: self) else { return nil }
-        return String(self[range])
-    }
+            guard nsRange.location >= 0, nsRange.length >= 0 else { return nil }
+            guard let range = Range(nsRange, in: self),
+                  range.lowerBound <= endIndex,
+                  range.upperBound <= endIndex 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 }

+ 9 - 2
NexilisLite/NexilisLite/Source/Model/Chat.swift

@@ -10,7 +10,7 @@ import Foundation
 public class Chat: Model {
     
     public let fpin: String
-    public let pin: String
+    public var pin: String
     public let messageId: String
     public var counter: String
     public var messageText: String
@@ -340,7 +340,7 @@ public class Chat: Model {
                             order by 6 desc
                             """
                 if !lastQuery.isEmpty {
-                    query = "select m.f_pin, m.opposite_pin, m.message_id, m.thumb_id, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, b.first_name || ' ' || ifnull(b.last_name, '') name, b.image_id profile, b.official_account, m.credential, m.lock, m.audio_id, m.gif_id from MESSAGE m JOIN BUDDY b ON m.f_pin = b.f_pin where \(lastQuery) order by 6 desc"
+                    query = "select m.f_pin, m.opposite_pin, m.message_id, m.thumb_id, m.message_text, m.server_date, m.image_id, m.video_id, m.file_id, m.attachment_flag, m.message_scope_id, b.first_name || ' ' || ifnull(b.last_name, '') name, b.image_id profile, b.official_account, m.credential, m.lock, m.audio_id, m.gif_id, m.l_pin, m.chat_id from MESSAGE m JOIN BUDDY b ON m.f_pin = b.f_pin where \(lastQuery) order by 6 desc"
                 }
                 if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
                     while cursorData.next() {
@@ -380,6 +380,13 @@ public class Chat: Model {
                                         groupName: cursorData.string(forColumnIndex: 20) ?? "",
                                         pinned: cursorData.longLongInt(forColumnIndex: 21),
                                         isBot: Int(cursorData.string(forColumnIndex: 22) ?? "0") ?? 0)
+                        if chat.pin.isEmpty && !lastQuery.isEmpty {
+                            chat.pin = cursorData.string(forColumnIndex: 18) ?? ""
+                            let chatId = cursorData.string(forColumnIndex: 19) ?? ""
+                            if !chatId.isEmpty {
+                                chat.pin = chatId
+                            }
+                        }
                         chats.append(chat)
                     }
                     cursorData.close()

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

@@ -19,7 +19,7 @@ import CryptoKit
 import WebKit
 
 public class Nexilis: NSObject {
-    public static var cpaasVersion = "5.0.73"
+    public static var cpaasVersion = "5.0.74"
     public static var sAPIKey = ""
     
     public static var ADDRESS = ""

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

@@ -3404,6 +3404,8 @@ class MediaViewerViewController: UIViewController, UIGestureRecognizerDelegate,
     var media: MediaType!
 
     public let backgroundView = UIView()
+    public var titleCustom = ""
+    public var subtitleCustom = ""
     private let scrollView = UIScrollView()
     private let imageView = UIImageView()
     private var statusBarBackgroundView: UIView!
@@ -3428,6 +3430,10 @@ class MediaViewerViewController: UIViewController, UIGestureRecognizerDelegate,
         edgesForExtendedLayout = .all
         extendedLayoutIncludesOpaqueBars = true
         navigationController?.navigationBar.isTranslucent = true
+        
+        if !titleCustom.isEmpty {
+            setNavigationTitle(title: titleCustom, subtitle: subtitleCustom)
+        }
 
         // Background view
         backgroundView.backgroundColor = .white
@@ -3483,6 +3489,27 @@ class MediaViewerViewController: UIViewController, UIGestureRecognizerDelegate,
             self.backgroundView.alpha = 1
         }
     }
+    
+    func setNavigationTitle(title: String, subtitle: String) {
+        let titleLabel = UILabel()
+        titleLabel.text = title
+        titleLabel.font = UIFont.systemFont(ofSize: 15)
+        titleLabel.textColor = .label
+        titleLabel.textAlignment = .center
+
+        let subtitleLabel = UILabel()
+        subtitleLabel.text = subtitle
+        subtitleLabel.font = UIFont.systemFont(ofSize: 12)
+        subtitleLabel.textColor = .secondaryLabel
+        subtitleLabel.textAlignment = .center
+
+        let stack = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
+        stack.axis = .vertical
+        stack.alignment = .center
+        stack.spacing = 0
+
+        navigationItem.titleView = stack
+    }
 
     private func configureMedia() {
         switch media! {

+ 9 - 7
NexilisLite/NexilisLite/Source/View/Chat/ChatWALikeVC.swift

@@ -119,15 +119,17 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
     private func reloadAllData() {
 //        print("reloadAllData")
         DispatchQueue.main.async { [weak self] in
-            if self?.timerReloadData == nil && !self!.isGettingData {
-                self?.refresh()
+            guard let self = self else { return }
+            if self.timerReloadData == nil && !self.isGettingData {
+                self.refresh()
             } else {
-                self?.timerReloadData?.invalidate()
-                self?.timerReloadData = nil
-                self?.timerReloadData = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
+                self.timerReloadData?.invalidate()
+                self.timerReloadData = nil
+                self.timerReloadData = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
                     if self != nil && !self!.isGettingData {
-                        self?.refresh()
-                        self?.timerReloadData = nil
+                        guard let self = self else { return }
+                        self.refresh()
+                        self.timerReloadData = nil
                     }
                 }
             }

+ 104 - 69
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -3524,69 +3524,79 @@ extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
     
     private func handleIndent(_ textView: UITextView, _ range: NSRange, _ text: String) -> Bool {
         guard let nsText = textView.text as NSString? else { return true }
+
+        // Ensure valid range
+        guard range.location <= nsText.length else { return true }
+
         let newText = nsText.replacingCharacters(in: range, with: text)
         var lines = newText.components(separatedBy: "\n")
-        
-        // Ensure range location is valid, considering Unicode scalars
+
         guard let textRange = Range(range, in: textView.text) else { return true }
         let prefixText = textView.text[..<textRange.lowerBound]
-        let affectedLineIndex = prefixText.components(separatedBy: "\n").count - 1
-        guard affectedLineIndex >= 0, affectedLineIndex < lines.count else { return true }
-        
+        let affectedLineIndex = max(prefixText.components(separatedBy: "\n").count - 1, 0)
+        guard affectedLineIndex < lines.count else { return true }
+
         let affectedLine = lines[affectedLineIndex]
-        // Auto-indent new lines based on previous line
+
+        // ---- Auto-indent new lines ----
         if text == "\n" {
             let previousLine = lines[affectedLineIndex]
-            
+
+            // Handle bullet points
             if previousLine.hasPrefix("  •") {
                 let newBullet = "\n  • "
-                textView.text = nsText.replacingCharacters(in: range, with: newBullet)
-                DispatchQueue.main.async {
-                    textView.selectedRange = NSRange(location: range.location + newBullet.utf16.count, length: 0)
-                }
+                safeReplaceText(in: textView, range: range, with: newBullet)
                 return false
             }
-            
+
+            // Handle numbered list continuation
             if let match = previousLine.range(of: #"^\s{2}(\d+)\."#, options: .regularExpression),
                let numberMatch = previousLine[match].components(separatedBy: ".").first,
                let number = Int(numberMatch.trimmingCharacters(in: .whitespaces)) {
-                
                 let newNumber = "\n  \(number + 1). "
-                textView.text = nsText.replacingCharacters(in: range, with: newNumber)
-                DispatchQueue.main.async {
-                    textView.selectedRange = NSRange(location: range.location + newNumber.utf16.count, length: 0)
-                }
+                safeReplaceText(in: textView, range: range, with: newNumber)
                 return false
             }
         }
-        
-        // Handle Backspace on Empty Bullet (Convert "  • " → "- ")
-        if text.isEmpty && affectedLine.trimmingCharacters(in: .whitespaces) == "•" {
-            lines[affectedLineIndex] = "- "  // Replace "  • " with "- "
-            textView.text = lines.joined(separator: "\n")
-            DispatchQueue.main.async {
-                textView.selectedRange = NSRange(location: range.location - 1, length: 0)
+
+        // ---- Handle backspace cases ----
+        if text.isEmpty {
+            // Empty bullet → "- "
+            if affectedLine.trimmingCharacters(in: .whitespaces) == "•" {
+                lines[affectedLineIndex] = "- "
+                updateTextView(textView, with: lines.joined(separator: "\n"), cursorOffset: -1)
+                return false
             }
-            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, newText.hasPrefix("  "), newText.substring(with: NSRange(location: range.location - 2, length: 2)) == "  " {
-            lines[affectedLineIndex] = affectedLine.trimmingCharacters(in: .whitespaces)
-            textView.text = lines.joined(separator: "\n")
-            DispatchQueue.main.async {
-                textView.selectedRange = NSRange(location: range.location - 2, length: 0)
+
+            // Bullet or number deletion checks, safely bounded
+            if range.location >= 2,
+               let twoChars = textView.text.substring(with: NSRange(location: range.location - 2, length: 2)),
+               twoChars == "  " {
+                lines[affectedLineIndex] = affectedLine.trimmingCharacters(in: .whitespaces)
+                updateTextView(textView, with: lines.joined(separator: "\n"), cursorOffset: -2)
+                return false
             }
-            return false
         }
+
         return true
     }
     
+    private func safeReplaceText(in textView: UITextView, range: NSRange, with newText: String) {
+        let nsText = textView.text as NSString
+        textView.text = nsText.replacingCharacters(in: range, with: newText)
+        DispatchQueue.main.async {
+            textView.selectedRange = NSRange(location: range.location + newText.utf16.count, length: 0)
+        }
+    }
+
+    private func updateTextView(_ textView: UITextView, with newText: String, cursorOffset: Int) {
+        textView.text = newText
+        DispatchQueue.main.async {
+            let newLoc = max(textView.selectedRange.location + cursorOffset, 0)
+            textView.selectedRange = NSRange(location: newLoc, length: 0)
+        }
+    }
+    
     private func handleRichText(_ textView: UITextView) {
         textView.attributedText = textView.text.richText(isEditing: true, group_id: self.dataGroup["group_id"]  as? String ?? "", listMentionInTextField: self.listMentionInTextField)
     }
@@ -5954,39 +5964,48 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
             }
             else {
                 messageText.attributedText = textChat.richText(group_id: self.dataGroup["group_id"]  as? String ?? "")
-                modifyText()
+                modifyText(at: indexPath)
             }
         } else {
             messageText.attributedText = textChat.richText(group_id: self.dataGroup["group_id"]  as? String ?? "")
-            modifyText()
+            modifyText(at: indexPath)
         }
         
-        func modifyText() {
-            if !textChat.isEmpty {
-                if textChat.contains("■"){
-                    textChat = textChat.components(separatedBy: "■")[0]
-                    textChat = textChat.trimmingCharacters(in: .whitespacesAndNewlines)
-                }
-                if !fileChat.isEmpty && dataMessages[indexPath.row]["lock"] as? String != "1" && dataMessages[indexPath.row]["lock"] as? String != "2" {
-                    textChat = textChat.components(separatedBy: "|")[1]
-                }
-                let finalAtribute = textChat.richText(group_id: self.dataGroup["group_id"]  as? String ?? "")
-                textChat = finalAtribute.string
-                let urlPattern = "(https?://|www\\.)\\S+"
-                if let regex = try? NSRegularExpression(pattern: urlPattern, options: []) {
-                    let matches = regex.matches(in: textChat, options: [], range: NSRange(textChat.startIndex..., in: textChat))
-                    
-                    for match in matches {
-                        if let range = Range(match.range, in: textChat) {
-                            let linkText = String(textChat[range])
-                            let nsRange = NSRange(range, in: textChat)
-                            finalAtribute.addAttribute(.link, value: linkText, range: nsRange)
-                            finalAtribute.addAttribute(.foregroundColor, value: UIColor.blue, range: nsRange)
-                            finalAtribute.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: nsRange)
-                        }
-                    }
+        func modifyText(at indexPath: IndexPath) {
+            guard !textChat.isEmpty else { return }
+            guard indexPath.row >= 0, indexPath.row < dataMessages.count else {
+                print("⚠️ modifyText: Invalid index \(indexPath.row), total: \(dataMessages.count)")
+                return
+            }
+            var text = textChat
+            let messageData = dataMessages[indexPath.row]
+            if let separatorRange = text.range(of: "■") {
+                text = String(text[..<separatorRange.lowerBound]).trimmingCharacters(in: .whitespacesAndNewlines)
+            }
+            if !fileChat.isEmpty {
+                let lock = messageData["lock"] as? String ?? ""
+                if lock != "1", lock != "2" {
+                    let parts = text.components(separatedBy: "|")
+                    if parts.count > 1 { text = parts[1] }
+                }
+            }
+            let finalAttributed = 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)
                 }
-                messageText.attributedText = finalAtribute
+            }
+            DispatchQueue.main.async {
+                messageText.attributedText = finalAttributed
                 messageText.delegate = self
             }
         }
@@ -7282,7 +7301,16 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
         let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
         let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
         let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
-        let indexPath = sender.indexPath
+        var indexPath = sender.indexPath
+        if indexPath.count == 0 {
+            if let index = self.dataMessages.firstIndex(where: {$0["message_id"] as? String == sender.message_id}) {
+                let section = self.dataDates.firstIndex(of: self.dataMessages[index]["chat_date"]  as? String ?? "")
+                let row = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == self.dataDates[section!]}).firstIndex(where: { $0["message_id"]  as? String ?? "" == self.dataMessages[index]["message_id"]  as? String ?? ""})
+                if row != nil && section != nil {
+                    indexPath = IndexPath(row: row!, section: section!)
+                }
+            }
+        }
         let dataMessages = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[indexPath.section]})
         func showMedia(data: Data? = nil, url: URL? = nil, type: Int = 0) {
             let image = UIImage(data: data ?? Data())
@@ -7323,8 +7351,15 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                 imageViewer.navigationItem.rightBarButtonItem = shareButton
             }
             
-            let name = (dataGroup["f_name"] as? String ?? "") + " (\(dataTopic["title"] as? String ?? ""))"
-            imageViewer.title = name
+            let dataProfile = getDataProfile(f_pin: dataMessages[indexPath.row]["f_pin"]  as? String ?? "", message_id: dataMessages[indexPath.row]["message_id"]  as? String ?? "")
+            let name = dataProfile["name"]
+            imageViewer.titleCustom = name ?? ""
+            if let timestamp = Double(dataMessages[indexPath.row][TypeDataMessage.server_date] as? String ?? "") {
+                let date = Date(timeIntervalSince1970: timestamp / 1000)
+                let formatter = DateFormatter()
+                formatter.dateFormat = "dd/MM/yy HH:mm"
+                imageViewer.subtitleCustom = formatter.string(from: date)
+            }
             
             let transitionDelegate = ZoomTransitioningDelegate()
             transitionDelegate.originImageView = sender.imageView

+ 103 - 85
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -4955,69 +4955,79 @@ extension EditorPersonal: UITextViewDelegate, CustomTextViewPasteDelegate {
     
     private func handleIndent(_ textView: UITextView, _ range: NSRange, _ text: String) -> Bool {
         guard let nsText = textView.text as NSString? else { return true }
+
+        // Ensure valid range
+        guard range.location <= nsText.length else { return true }
+
         let newText = nsText.replacingCharacters(in: range, with: text)
         var lines = newText.components(separatedBy: "\n")
-        
-        // Ensure range location is valid, considering Unicode scalars
+
         guard let textRange = Range(range, in: textView.text) else { return true }
         let prefixText = textView.text[..<textRange.lowerBound]
-        let affectedLineIndex = prefixText.components(separatedBy: "\n").count - 1
-        guard affectedLineIndex >= 0, affectedLineIndex < lines.count else { return true }
-        
+        let affectedLineIndex = max(prefixText.components(separatedBy: "\n").count - 1, 0)
+        guard affectedLineIndex < lines.count else { return true }
+
         let affectedLine = lines[affectedLineIndex]
-        // Auto-indent new lines based on previous line
+
+        // ---- Auto-indent new lines ----
         if text == "\n" {
             let previousLine = lines[affectedLineIndex]
-            
+
+            // Handle bullet points
             if previousLine.hasPrefix("  •") {
                 let newBullet = "\n  • "
-                textView.text = nsText.replacingCharacters(in: range, with: newBullet)
-                DispatchQueue.main.async {
-                    textView.selectedRange = NSRange(location: range.location + newBullet.utf16.count, length: 0)
-                }
+                safeReplaceText(in: textView, range: range, with: newBullet)
                 return false
             }
-            
+
+            // Handle numbered list continuation
             if let match = previousLine.range(of: #"^\s{2}(\d+)\."#, options: .regularExpression),
                let numberMatch = previousLine[match].components(separatedBy: ".").first,
                let number = Int(numberMatch.trimmingCharacters(in: .whitespaces)) {
-                
                 let newNumber = "\n  \(number + 1). "
-                textView.text = nsText.replacingCharacters(in: range, with: newNumber)
-                DispatchQueue.main.async {
-                    textView.selectedRange = NSRange(location: range.location + newNumber.utf16.count, length: 0)
-                }
+                safeReplaceText(in: textView, range: range, with: newNumber)
                 return false
             }
         }
-        
-        // Handle Backspace on Empty Bullet (Convert "  • " → "- ")
-        if text.isEmpty && affectedLine.trimmingCharacters(in: .whitespaces) == "•" {
-            lines[affectedLineIndex] = "- "  // Replace "  • " with "- "
-            textView.text = lines.joined(separator: "\n")
-            DispatchQueue.main.async {
-                textView.selectedRange = NSRange(location: range.location - 1, length: 0)
+
+        // ---- Handle backspace cases ----
+        if text.isEmpty {
+            // Empty bullet → "- "
+            if affectedLine.trimmingCharacters(in: .whitespaces) == "•" {
+                lines[affectedLineIndex] = "- "
+                updateTextView(textView, with: lines.joined(separator: "\n"), cursorOffset: -1)
+                return false
             }
-            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, newText.hasPrefix("  "), newText.substring(with: NSRange(location: range.location - 2, length: 2)) == "  " {
-            lines[affectedLineIndex] = affectedLine.trimmingCharacters(in: .whitespaces)
-            textView.text = lines.joined(separator: "\n")
-            DispatchQueue.main.async {
-                textView.selectedRange = NSRange(location: range.location - 2, length: 0)
+
+            // Bullet or number deletion checks, safely bounded
+            if range.location >= 2,
+               let twoChars = textView.text.substring(with: NSRange(location: range.location - 2, length: 2)),
+               twoChars == "  " {
+                lines[affectedLineIndex] = affectedLine.trimmingCharacters(in: .whitespaces)
+                updateTextView(textView, with: lines.joined(separator: "\n"), cursorOffset: -2)
+                return false
             }
-            return false
         }
+
         return true
     }
     
+    private func safeReplaceText(in textView: UITextView, range: NSRange, with newText: String) {
+        let nsText = textView.text as NSString
+        textView.text = nsText.replacingCharacters(in: range, with: newText)
+        DispatchQueue.main.async {
+            textView.selectedRange = NSRange(location: range.location + newText.utf16.count, length: 0)
+        }
+    }
+
+    private func updateTextView(_ textView: UITextView, with newText: String, cursorOffset: Int) {
+        textView.text = newText
+        DispatchQueue.main.async {
+            let newLoc = max(textView.selectedRange.location + cursorOffset, 0)
+            textView.selectedRange = NSRange(location: newLoc, length: 0)
+        }
+    }
+    
     private func handleRichText(_ textView: UITextView) {
         textView.attributedText = textView.text.richText(isEditing: true, listMentionInTextField: self.listMentionInTextField)
     }
@@ -7665,39 +7675,48 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
             }
             else {
                 messageText.attributedText = textChat.richText()
-                modifyText()
+                modifyText(at: indexPath)
             }
         } else {
             messageText.attributedText = textChat.richText()
-            modifyText()
+            modifyText(at: indexPath)
         }
         
-        func modifyText() {
-            if !textChat.isEmpty {
-                if textChat.contains("■"){
-                    textChat = textChat.components(separatedBy: "■")[0]
-                    textChat = textChat.trimmingCharacters(in: .whitespacesAndNewlines)
-                }
-                if !fileChat.isEmpty && dataMessages[indexPath.row]["lock"] as? String != "1" && dataMessages[indexPath.row]["lock"] as? String != "2" {
-                    textChat = textChat.components(separatedBy: "|")[1]
-                }
-                let finalAtribute = textChat.richText()
-                textChat = finalAtribute.string
-                let urlPattern = "(https?://|www\\.)\\S+"
-                if let regex = try? NSRegularExpression(pattern: urlPattern, options: []) {
-                    let matches = regex.matches(in: textChat, options: [], range: NSRange(textChat.startIndex..., in: textChat))
-                    
-                    for match in matches {
-                        if let range = Range(match.range, in: textChat) {
-                            let linkText = String(textChat[range])
-                            let nsRange = NSRange(range, in: textChat)
-                            finalAtribute.addAttribute(.link, value: linkText, range: nsRange)
-                            finalAtribute.addAttribute(.foregroundColor, value: UIColor.blue, range: nsRange)
-                            finalAtribute.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: nsRange)
-                        }
-                    }
+        func modifyText(at indexPath: IndexPath) {
+            guard !textChat.isEmpty else { return }
+            guard indexPath.row >= 0, indexPath.row < dataMessages.count else {
+                print("⚠️ modifyText: Invalid index \(indexPath.row), total: \(dataMessages.count)")
+                return
+            }
+            var text = textChat
+            let messageData = dataMessages[indexPath.row]
+            if let separatorRange = text.range(of: "■") {
+                text = String(text[..<separatorRange.lowerBound]).trimmingCharacters(in: .whitespacesAndNewlines)
+            }
+            if !fileChat.isEmpty {
+                let lock = messageData["lock"] as? String ?? ""
+                if lock != "1", lock != "2" {
+                    let parts = text.components(separatedBy: "|")
+                    if parts.count > 1 { text = parts[1] }
+                }
+            }
+            let finalAttributed = 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)
                 }
-                messageText.attributedText = finalAtribute
+            }
+            DispatchQueue.main.async {
+                messageText.attributedText = finalAttributed
                 messageText.delegate = self
             }
         }
@@ -9108,7 +9127,16 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
         let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
         let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
         let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
-        let indexPath = sender.indexPath
+        var indexPath = sender.indexPath
+        if indexPath.count == 0 {
+            if let index = self.dataMessages.firstIndex(where: {$0["message_id"] as? String == sender.message_id}) {
+                let section = self.dataDates.firstIndex(of: self.dataMessages[index]["chat_date"]  as? String ?? "")
+                let row = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == self.dataDates[section!]}).firstIndex(where: { $0["message_id"]  as? String ?? "" == self.dataMessages[index]["message_id"]  as? String ?? ""})
+                if row != nil && section != nil {
+                    indexPath = IndexPath(row: row!, section: section!)
+                }
+            }
+        }
         let dataMessages = self.dataMessages.filter({ $0["chat_date"]  as? String ?? "" == dataDates[indexPath.section]})
         func showMedia(data: Data? = nil, url: URL? = nil, type: Int = 0) {
             let image = UIImage(data: data ?? Data())
@@ -9149,25 +9177,15 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 imageViewer.navigationItem.rightBarButtonItem = shareButton
             }
             
-            var name = ""
-            if !isContactCenter {
-                name = dataPerson["name"] as? String ?? ""
-            } else {
-                if users.count == 1 {
-                    name = users[0].fullName
-                } else {
-                    var stringName = ""
-                    for user in users {
-                        if stringName.isEmpty {
-                            stringName = user.fullName
-                        } else {
-                            stringName += ", \(user.fullName)"
-                        }
-                    }
-                    name = stringName
-                }
+            let user = User.getData(pin: dataMessages[indexPath.row]["f_pin"] as? String)
+            let name = user?.fullName
+            imageViewer.titleCustom = name ?? ""
+            if let timestamp = Double(dataMessages[indexPath.row][TypeDataMessage.server_date] as? String ?? "") {
+                let date = Date(timeIntervalSince1970: timestamp / 1000)
+                let formatter = DateFormatter()
+                formatter.dateFormat = "dd/MM/yy HH:mm"
+                imageViewer.subtitleCustom = formatter.string(from: date)
             }
-            imageViewer.title = name
             
             let transitionDelegate = ZoomTransitioningDelegate()
             transitionDelegate.originImageView = sender.imageView

+ 51 - 38
NexilisLite/NexilisLite/Source/View/Chat/EditorStarMessages.swift

@@ -347,6 +347,12 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
             imageStared.tintColor = .systemYellow
         }
         
+        let thumbChat = (dataMessages[indexPath.row]["thumb_id"] as? String) ?? ""
+        let imageChat = (dataMessages[indexPath.row]["image_id"] as? String) ?? ""
+        let videoChat = (dataMessages[indexPath.row]["video_id"] as? String) ?? ""
+        let fileChat = (dataMessages[indexPath.row]["file_id"] as? String) ?? ""
+        let gifChat = (dataMessages[indexPath.row]["gif_id"] as? String) ?? ""
+        
         let imageAckView = UIImageView()
         if dataMessages[indexPath.row]["read_receipts"] as? String == "8" {
             var imageAck = UIImage(named: "ack_icon_gray", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal)
@@ -416,7 +422,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
         messageText.bottomAnchor.constraint(equalTo: containerMessage.bottomAnchor, constant: -15).isActive = true
         messageText.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
         
-        var textChat = (dataMessages[indexPath.row]["message_text"])! as? String
+        var textChat = (dataMessages[indexPath.row]["message_text"])! as? String ?? ""
         if (dataMessages[indexPath.row]["lock"] != nil && (dataMessages[indexPath.row]["lock"])! as? String == "1") {
             if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
                 textChat = "🚫 _"+"You were deleted this message".localized()+"_"
@@ -426,7 +432,8 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
         }
         let imageSticker = UIImageView()
         if let attachmentFlag = dataMessages[indexPath.row]["attachment_flag"], let attachmentFlag = attachmentFlag as? String {
-            if attachmentFlag == "27" || attachmentFlag == "26", let data = textChat { // live streaming
+            if attachmentFlag == "27" || attachmentFlag == "26" {
+                let data = textChat// live streaming
                 if let json = try! JSONSerialization.jsonObject(with: data.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
                     Database.shared.database?.inTransaction({ fmdb, rollback in
                         let title = json["title"] as! String
@@ -456,44 +463,56 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                 imageSticker.bottomAnchor.constraint(equalTo: messageText.topAnchor, constant: -5).isActive = true
                 imageSticker.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor).isActive = true
                 imageSticker.widthAnchor.constraint(equalToConstant: 80).isActive = true
-                var imageStickerBundle = UIImage(named: (textChat!.components(separatedBy: "/")[1]), in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+                var imageStickerBundle = UIImage(named: (textChat.components(separatedBy: "/")[1]), in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
                 if imageStickerBundle == nil {
-                    imageStickerBundle = UIImage(named: (textChat!.components(separatedBy: "/")[1]), in: Bundle.resourcesMediaBundle(for: Nexilis.self), with: nil)
+                    imageStickerBundle = UIImage(named: (textChat.components(separatedBy: "/")[1]), in: Bundle.resourcesMediaBundle(for: Nexilis.self), with: nil)
                 }
                 imageSticker.image = imageStickerBundle //resourcesMediaBundle
                 imageSticker.contentMode = .scaleAspectFit
             }
             else {
-                modifyText()
+                modifyText(at: indexPath)
             }
         } else {
-            modifyText()
+            modifyText(at: indexPath)
         }
         messageText.font = UIFont.systemFont(ofSize: 12 + offset())
         
-        func modifyText() {
-            if !textChat!.isEmpty {
-                if textChat!.contains("■"){
-                    textChat = textChat!.components(separatedBy: "■")[0]
-                    textChat = textChat!.trimmingCharacters(in: .whitespacesAndNewlines)
+        func modifyText(at indexPath: IndexPath) {
+            guard !textChat.isEmpty else { return }
+            guard indexPath.row >= 0, indexPath.row < dataMessages.count else {
+                print("⚠️ modifyText: Invalid index \(indexPath.row), total: \(dataMessages.count)")
+                return
+            }
+            var text = textChat
+            let messageData = dataMessages[indexPath.row]
+            if let separatorRange = text.range(of: "■") {
+                text = String(text[..<separatorRange.lowerBound]).trimmingCharacters(in: .whitespacesAndNewlines)
+            }
+            if !fileChat.isEmpty {
+                let lock = messageData["lock"] as? String ?? ""
+                if lock != "1", lock != "2" {
+                    let parts = text.components(separatedBy: "|")
+                    if parts.count > 1 { text = parts[1] }
                 }
-                let finalAtribute = textChat!.richText()
-                textChat = finalAtribute.string
-                let urlPattern = "(https?://|www\\.)\\S+"
-                if let regex = try? NSRegularExpression(pattern: urlPattern, options: []) {
-                    let matches = regex.matches(in: textChat!, options: [], range: NSRange(textChat!.startIndex..., in: textChat!))
-                    
-                    for match in matches {
-                        if let range = Range(match.range, in: textChat!) {
-                            let linkText = String(textChat![range])
-                            let nsRange = NSRange(range, in: textChat!)
-                            finalAtribute.addAttribute(.link, value: linkText, range: nsRange)
-                            finalAtribute.addAttribute(.foregroundColor, value: UIColor.blue, range: nsRange)
-                            finalAtribute.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: nsRange)
-                        }
-                    }
+            }
+            let finalAttributed = 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)
                 }
-                messageText.attributedText = finalAtribute
+            }
+            DispatchQueue.main.async {
+                messageText.attributedText = finalAttributed
                 messageText.delegate = self
             }
         }
@@ -513,12 +532,6 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
         timeMessage.font = UIFont.systemFont(ofSize: 10 + offset(), weight: .medium)
         timeMessage.textColor = .lightGray
         
-        let thumbChat = (dataMessages[indexPath.row]["thumb_id"] as? String) ?? ""
-        let imageChat = (dataMessages[indexPath.row]["image_id"] as? String) ?? ""
-        let videoChat = (dataMessages[indexPath.row]["video_id"] as? String) ?? ""
-        let fileChat = (dataMessages[indexPath.row]["file_id"] as? String) ?? ""
-        let gifChat = (dataMessages[indexPath.row]["gif_id"] as? String) ?? ""
-        
         let imageThumb = UIImageView()
         let containerViewFile = UIView()
         
@@ -709,8 +722,8 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
             let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
             let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
             let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
-            let arrExtFile = (textChat?.components(separatedBy: "|")[0])?.split(separator: ".")
-            let finalExtFile = arrExtFile![arrExtFile!.count - 1]
+            let arrExtFile = (textChat.components(separatedBy: "|")[0]).split(separator: ".")
+            let finalExtFile = arrExtFile[arrExtFile.count - 1]
             if let dirPath = paths.first {
                 let fileURL = URL(fileURLWithPath: dirPath).appendingPathComponent(fileChat)
                 if FileManager.default.fileExists(atPath: fileURL.path) {
@@ -790,7 +803,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
             nameFile.widthAnchor.constraint(lessThanOrEqualToConstant: 200).isActive = true
             nameFile.font = UIFont.systemFont(ofSize: 12 + offset(), weight: .medium)
             nameFile.textColor = .white
-            nameFile.text = textChat?.components(separatedBy: "|")[0]
+            nameFile.text = textChat.components(separatedBy: "|")[0]
             
             if (dataMessages[indexPath.row]["progress"] as! Double != 100.0) {
                 let containerLoading = UIView()
@@ -835,9 +848,9 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
         }
         
         let containerLinkMessage = UIView()
-        if thumbChat.isEmpty && fileChat.isEmpty && !textChat!.isEmpty {
+        if thumbChat.isEmpty && fileChat.isEmpty && !textChat.isEmpty {
             var text = ""
-            let listTextSplitBreak = textChat!.components(separatedBy: "\n")
+            let listTextSplitBreak = textChat.components(separatedBy: "\n")
             let indexFirstLinkSplitBreak = listTextSplitBreak.firstIndex(where: { $0.contains("www.") || $0.contains("http://") || $0.contains("https://") })
             if indexFirstLinkSplitBreak != nil {
                 let listTextSplitSpace = listTextSplitBreak[indexFirstLinkSplitBreak!].components(separatedBy: " ")

+ 95 - 23
NexilisLite/NexilisLite/Source/View/Chat/SecureFolderView.swift

@@ -18,7 +18,7 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
     var previewItem: NSURL?
 
     var isGridView: Bool = true
-    var isTab = true
+    var isTab = false
     
     public init(isTab: Bool = false) {
         self.isTab = isTab
@@ -31,6 +31,7 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
 
     let searchBar: UISearchBar = {
         let sb = UISearchBar()
+        sb.backgroundImage = UIImage()
         sb.placeholder = "Search files"
         return sb
     }()
@@ -59,8 +60,6 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
         tapGesture.cancelsTouchesInView = false
         view.addGestureRecognizer(tapGesture)
         setupSubviews()
-        loadFiles()
-        filteredFiles = files
     }
     
     @objc func dismissKeyboard() {
@@ -113,6 +112,10 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
 //        self.title = "Secure Folder".localized()
         self.navigationController?.navigationBar.topItem?.title = "Secure Folder".localized()
         self.navigationController?.navigationBar.setNeedsLayout()
+        loadFiles()
+        filteredFiles = files
+        collectionView.collectionViewLayout.invalidateLayout()
+        collectionView.reloadData()
     }
     
     @objc func cancel(sender: Any) {
@@ -164,8 +167,10 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
     }
     
     func loadFiles() {
+        files.removeAll()
+        filteredFiles.removeAll()
         do {
-            var query = "SELECT audio_id, video_id, image_id, thumb_id, file_id, attachment_flag, status, server_date FROM MESSAGE where (video_id IS NOT NULL AND video_id != '') OR (image_id IS NOT NULL AND image_id != '') OR (file_id IS NOT NULL AND file_id != '') order by server_date asc"
+            let query = "SELECT audio_id, video_id, image_id, thumb_id, file_id, attachment_flag, status, server_date, message_text FROM MESSAGE where (video_id IS NOT NULL AND video_id != '') OR (image_id IS NOT NULL AND image_id != '') OR (file_id IS NOT NULL AND file_id != '') order by server_date asc"
             Database.shared.database?.inTransaction({ (fmdb, rollback) in
                 do {
                     if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
@@ -179,19 +184,38 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
                             let attachmentFlag = cursorData.string(forColumn: "attachment_flag") ?? ""
                             let status = cursorData.string(forColumn: "status") ?? ""
                             let serverDate = cursorData.string(forColumn: "server_date") ?? ""
+                            let messageText = cursorData.string(forColumn: "message_text") ?? ""
+                            var dataFile = ""
                             if imageId != "" {
-                                fileName = imageId
+                                if messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+                                    fileName = "📷 " + "Photo".localized()
+                                } else {
+                                    fileName = imageId
+                                }
+                                dataFile = imageId
                             }
                             else if videoId != "" {
-                                fileName = videoId
+                                if messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+                                    fileName = "📹 " + "Video".localized()
+                                } else {
+                                    fileName = videoId
+                                }
+                                dataFile = videoId
                             }
                             else if fileId != "" {
-                                fileName = fileId
+                                let arrayMessage = messageText.components(separatedBy: "|")
+                                if arrayMessage.count > 1 {
+                                    fileName = messageText.components(separatedBy: "|")[0]
+                                } else {
+                                    fileName = fileId
+                                }
+                                dataFile = fileId
                             }
                             else if audioId != "" {
-                                fileName = audioId
+                                fileName = "♫ " + "Audio".localized()
+                                dataFile = audioId
                             }
-                            if FileEncryption.shared.isSecureExists(filename: fileName) {
+                            if FileEncryption.shared.isSecureExists(filename: dataFile) {
                                 let secureFolderItem = SecureFolderItem(audioId: audioId, videoId: videoId, imageId: imageId, fileId: fileId, thumbId: thumbId, attachmentFlag: attachmentFlag, serverDate: serverDate, status: status, filename: fileName)
                                 files.append(secureFolderItem)
                             }
@@ -273,10 +297,17 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
     public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
         let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FileCell", for: indexPath) as! FileCell
         let fileItem = filteredFiles[indexPath.item]
+        var thumbnailImage = UIImage(systemName: "doc.text")?.withTintColor(.black)
+        if fileItem.thumbId != "" {
+            thumbnailImage = getThumbnail(for: fileItem.thumbId)
+        } else if fileItem.audioId != "" {
+            thumbnailImage = UIImage(systemName: "speaker.wave.3")?.withTintColor(.black)
+        }
+        cell.imageView.image = thumbnailImage
         if fileItem.imageId != "" {
             print("this image")
             do {
-                if var data = try FileEncryption.shared.readSecure(filename: fileItem.filename) {
+                if var data = try FileEncryption.shared.readSecure(filename: fileItem.imageId) {
                     let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
                     if dataDecrypt != nil {
                         data = dataDecrypt!
@@ -291,7 +322,7 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
         else if fileItem.videoId != "" {
             print("this video")
             do {
-                if var secureData = try FileEncryption.shared.readSecure(filename: fileItem.filename) {
+                if var secureData = try FileEncryption.shared.readSecure(filename: fileItem.videoId) {
                     let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: secureData)
                     if dataDecrypt != nil {
                         secureData = dataDecrypt!
@@ -299,11 +330,7 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
                     let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
                     let tempPath = cachesDirectory.appendingPathComponent(fileItem.filename)
                     try secureData.write(to: tempPath)
-                    let player = AVPlayer(url: tempPath as URL)
-                    let playerVC = AVPlayerViewController()
-                    playerVC.modalPresentationStyle = .custom
-                    playerVC.player = player
-                    self.present(playerVC, animated: true, completion: nil)
+                    APIS.openVideoNexilis(imageView: cell.imageView, videoURL: tempPath)
                 }
             } catch {
                 
@@ -312,7 +339,57 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
         else if fileItem.fileId != "" {
             print("this file")
             do {
-                if var docData = try FileEncryption.shared.readSecure(filename: fileItem.filename) {
+                func showFile(urlFile: URL, isFile: Bool = true) {
+                    let previewController = QLPreviewController()
+                    previewController.dataSource = self
+                    let vcHandleFile = UIViewController()
+                    let nc = UINavigationController(rootViewController: vcHandleFile)
+                    let attributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
+                    let navBarAppearance = UINavigationBarAppearance()
+                    nc.defaultStyle()
+                    navBarAppearance.configureWithOpaqueBackground()
+                    navBarAppearance.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : UIColor.mainColor
+                    navBarAppearance.titleTextAttributes = attributes
+                    nc.navigationBar.standardAppearance = navBarAppearance
+                    nc.navigationBar.scrollEdgeAppearance = navBarAppearance
+                    let backAction = UIAction { _ in
+                        nc.dismiss(animated: true)
+                    }
+                    let backButton = UIBarButtonItem(title: nil, image: UIImage(systemName: "chevron.backward"), primaryAction: backAction, menu: nil)
+                    vcHandleFile.navigationItem.leftBarButtonItem = backButton
+                    if Nexilis.checkingAccess(key: "secure_folder_share") {
+                        let shareAction = UIAction { _ in
+                            let fileManager = FileManager.default
+                            let tempURL = fileManager.temporaryDirectory.appendingPathComponent(urlFile.lastPathComponent)
+                            do {
+                                if !fileManager.fileExists(atPath: tempURL.path) {
+                                    try fileManager.copyItem(at: urlFile, to: tempURL)
+                                }
+                                let activityViewController = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil)
+                                activityViewController.popoverPresentationController?.sourceView = vcHandleFile.view
+                                vcHandleFile.present(activityViewController, animated: true, completion: nil)
+                            } catch {
+                                
+                            }
+                        }
+                        let shareButton = UIBarButtonItem(title: nil, image: UIImage(systemName: "square.and.arrow.up"), primaryAction: shareAction, menu: nil)
+                        vcHandleFile.navigationItem.rightBarButtonItem = shareButton
+                    }
+                    if let viewVc = vcHandleFile.view {
+                        if isFile {
+                            vcHandleFile.title = fileItem.filename
+                        }
+                        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.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+                        viewVc.addSubview(previewController.view)
+                        previewController.didMove(toParent: vcHandleFile)
+                        
+                        self.present(nc, animated: true)
+                    }
+                }
+                if var docData = try FileEncryption.shared.readSecure(filename: fileItem.fileId) {
                     let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: docData)
                     if dataDecrypt != nil {
                         docData = dataDecrypt!
@@ -321,12 +398,7 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
                     let tempPath = cachesDirectory.appendingPathComponent(fileItem.filename)
                     try docData.write(to: tempPath)
                     self.previewItem = tempPath as NSURL
-                    let previewController = QLPreviewController()
-                    let rightBarButton = UIBarButtonItem()
-                    previewController.navigationItem.rightBarButtonItem = rightBarButton
-                    previewController.dataSource = self
-                    previewController.modalPresentationStyle = .custom
-                    self.present(previewController,animated: true)
+                    showFile(urlFile: tempPath)
                 }
             }
             catch {

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

@@ -275,15 +275,17 @@ class ContactChatViewController: UITableViewController {
     private func reloadAllData() {
 //        print("reloadAllData")
         DispatchQueue.main.async { [weak self] in
-            if self?.timerReloadData == nil && !self!.isGettingData {
-                self?.getData()
+            guard let self = self else { return }
+            if self.timerReloadData == nil && !self.isGettingData {
+                self.getData()
             } else {
-                self?.timerReloadData?.invalidate()
-                self?.timerReloadData = nil
-                self?.timerReloadData = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
+                self.timerReloadData?.invalidate()
+                self.timerReloadData = nil
+                self.timerReloadData = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
                     if self != nil && !self!.isGettingData {
-                        self?.getData()
-                        self?.timerReloadData = nil
+                        guard let self = self else { return }
+                        self.getData()
+                        self.timerReloadData = nil
                     }
                 }
             }