浏览代码

add view grouping images

alqindiirsyam 2 年之前
父节点
当前提交
646689a9fc

+ 4 - 0
appbuilder-ios/DigiXLite/DigiXLite.xcodeproj/project.pbxproj

@@ -188,6 +188,7 @@
 		CD46A0BB2A0CE320009E4C87 /* sticker_10000000_8.png in Resources */ = {isa = PBXBuildFile; fileRef = CD46A0162A0CE2DE009E4C87 /* sticker_10000000_8.png */; };
 		CD46A0BF2A0CE4FD009E4C87 /* DigiXLite.podspec in Resources */ = {isa = PBXBuildFile; fileRef = CD46A0BE2A0CE4FD009E4C87 /* DigiXLite.podspec */; };
 		CD46A0C52A0D0D5D009E4C87 /* MyArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD46A0C42A0D0D5D009E4C87 /* MyArchive.swift */; };
+		CD5A73AB2A736761000541A5 /* ListGroupImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD5A73AA2A736761000541A5 /* ListGroupImages.swift */; };
 		CD95516C2A6688ED00AF6476 /* Inter-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = CDAEC1172A612981007DE08D /* Inter-LightItalic.ttf */; };
 		CD95516D2A6688ED00AF6476 /* Inter-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = CDAEC1182A612981007DE08D /* Inter-ExtraBold.ttf */; };
 		CD95516E2A6688ED00AF6476 /* Inter-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = CDAEC11F2A612981007DE08D /* Inter-Italic.ttf */; };
@@ -441,6 +442,7 @@
 		CD46A01A2A0CE2DE009E4C87 /* Palio.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Palio.storyboard; sourceTree = "<group>"; };
 		CD46A0BE2A0CE4FD009E4C87 /* DigiXLite.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = DigiXLite.podspec; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
 		CD46A0C42A0D0D5D009E4C87 /* MyArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyArchive.swift; sourceTree = "<group>"; };
+		CD5A73AA2A736761000541A5 /* ListGroupImages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListGroupImages.swift; sourceTree = "<group>"; };
 		CD9829B62A3C07CB009F6743 /* SeminarListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeminarListViewController.swift; sourceTree = "<group>"; };
 		CDAEC1102A612981007DE08D /* Inter-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Medium.ttf"; sourceTree = "<group>"; };
 		CDAEC1112A612981007DE08D /* Inter-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Light.ttf"; sourceTree = "<group>"; };
@@ -596,6 +598,7 @@
 				CD1E71502A0BA86100BF871F /* WhiteboardCanvas.swift */,
 				CD1E71AF2A0BA86100BF871F /* WhiteboardDelegate.swift */,
 				CD1E715B2A0BA86100BF871F /* WhiteboardReceiver.swift */,
+				CD5A73AA2A736761000541A5 /* ListGroupImages.swift */,
 			);
 			path = Source;
 			sourceTree = "<group>";
@@ -1238,6 +1241,7 @@
 				CD1E724A2A0BA86100BF871F /* HistoryCCViewController.swift in Sources */,
 				CD1E722A2A0BA86100BF871F /* StreamingViewController.swift in Sources */,
 				CD1E72132A0BA86100BF871F /* Database.swift in Sources */,
+				CD5A73AB2A736761000541A5 /* ListGroupImages.swift in Sources */,
 				CD1E720F2A0BA86100BF871F /* Download.swift in Sources */,
 				CD1E725D2A0BA86100BF871F /* WhiteboardDelegate.swift in Sources */,
 				CD1E721B2A0BA86100BF871F /* DigiX.swift in Sources */,

二进制
appbuilder-ios/DigiXLite/DigiXLite.xcworkspace/xcuserdata/akhmadalqindiirsyam.xcuserdatad/UserInterfaceState.xcuserstate


+ 41 - 0
appbuilder-ios/DigiXLite/DigiXLite/Source/Extension.swift

@@ -1254,3 +1254,44 @@ extension String {
         lhs = lhs + rhs
     }
 }
+
+extension UIGraphicsRenderer {
+    static func renderImagesAt(urls: [NSURL], size: CGSize, scale: CGFloat = 1) -> UIImage {
+        let renderer = UIGraphicsImageRenderer(size: size)
+
+        let options: [NSString: Any] = [
+            kCGImageSourceThumbnailMaxPixelSize: max(size.width * scale, size.height * scale),
+            kCGImageSourceCreateThumbnailFromImageAlways: true
+        ]
+
+        let thumbnails = try urls.map { url -> CGImage in
+            let imageSource = CGImageSourceCreateWithURL(url, nil)
+
+            let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource!, 0, options as CFDictionary)
+            
+            return scaledImage!
+        }
+
+        // Translate Y-axis down because cg images are flipped and it falls out of the frame (see bellow)
+        let rect = CGRect(x: 0,
+                          y: -size.height,
+                          width: size.width,
+                          height: size.height)
+
+        let resizedImage = renderer.image { ctx in
+
+            let context = ctx.cgContext
+            context.scaleBy(x: 1, y: -1) //Flip it ( cg y-axis is flipped)
+
+            for image in thumbnails {
+                context.draw(image, in: rect)
+            }
+        }
+
+        return resizedImage
+    }
+    
+    static func renderImageAt(url: NSURL, size: CGSize, scale: CGFloat = 1) -> UIImage {
+            return renderImagesAt(urls: [url], size: size, scale: scale)
+        }
+}

+ 68 - 0
appbuilder-ios/DigiXLite/DigiXLite/Source/ListGroupImages.swift

@@ -0,0 +1,68 @@
+//
+//  ListGroupImages.swift
+//  DigiXLite
+//
+//  Created by Akhmad Al Qindi Irsyam on 28/07/23.
+//
+
+import UIKit
+
+class ListGroupImages: UIViewController {
+    var listGroupingImages: [ImageGrouping]!
+    var imageTapped: Int!
+    var titleName: String!
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        view.backgroundColor = .white
+
+        let centeredTitleView = CenteredTitleSubtitleView(frame: CGRect(x: 0, y: 0, width: 200, height: 44))
+        centeredTitleView.titleLabel.text = titleName
+        centeredTitleView.subtitleLabel.text = String(listGroupingImages.count) + " " + "images".localized()
+        navigationItem.titleView = centeredTitleView
+    }
+}
+
+class CenteredTitleSubtitleView: UIView {
+    let titleLabel: UILabel = {
+        let label = UILabel()
+        label.textAlignment = .center
+        label.font = UIFont.boldSystemFont(ofSize: 18)
+        label.textColor = .white
+        return label
+    }()
+    
+    let subtitleLabel: UILabel = {
+        let label = UILabel()
+        label.textAlignment = .center
+        label.font = UIFont.systemFont(ofSize: 14)
+        label.textColor = .lightGray
+        return label
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupSubviews()
+    }
+    
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        setupSubviews()
+    }
+    
+    private func setupSubviews() {
+        addSubview(titleLabel)
+        addSubview(subtitleLabel)
+        
+        // Add any constraints or frames you prefer
+        // Here's an example using autolayout anchors
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
+        titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
+        titleLabel.topAnchor.constraint(equalTo: topAnchor).isActive = true
+        
+        subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
+        subtitleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
+        subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true
+    }
+}

+ 9 - 3
appbuilder-ios/DigiXLite/DigiXLite/Source/View/Chat/EditorGroup.swift

@@ -98,6 +98,9 @@ public class EditorGroup: UIViewController {
         if self.isMovingFromParent {
             UserDefaults.standard.removeObject(forKey: "inEditorGroup")
             NotificationCenter.default.removeObserver(self)
+            super.viewDidDisappear(true)
+            self.removeFromParent()
+            self.dismiss(animated: true, completion: nil)
         }
     }
     
@@ -3461,7 +3464,8 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
             let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
             if let dirPath = paths.first {
                 let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumbChat)
-                let image    = UIImage(contentsOfFile: thumbURL.path)
+//                let image    = UIImage(contentsOfFile: thumbURL.path)
+                let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
                 imageThumb.image = image
                 
                 let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(videoChat)
@@ -3883,7 +3887,8 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                     let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
                     if let dirPath = paths.first {
                         let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumb_chat)
-                        let image    = UIImage(contentsOfFile: thumbURL.path)
+//                        let image    = UIImage(contentsOfFile: thumbURL.path)
+                        let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
                         let imageThumb = UIImageView(image: image)
                         containerReply.addSubview(imageThumb)
                         imageThumb.layer.cornerRadius = 2.0
@@ -4458,7 +4463,8 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
             let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
             if let dirPath = paths.first {
                 let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumb_chat)
-                let image    = UIImage(contentsOfFile: thumbURL.path)
+//                let image    = UIImage(contentsOfFile: thumbURL.path)
+                let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
                 let imageThumb = UIImageView(image: image)
                 self.containerPreviewReply.addSubview(imageThumb)
                 imageThumb.layer.cornerRadius = 2.0

+ 254 - 93
appbuilder-ios/DigiXLite/DigiXLite/Source/View/Chat/EditorPersonal.swift

@@ -106,6 +106,8 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     var isDirectCC = false
     var fakeProgMultip = 0
     let maxFakeProgMultip = 2
+    var groupImages: [String:[ImageGrouping]] = [:]
+    var titleText: String!
     
     public override func viewDidDisappear(_ animated: Bool) {
         if self.isMovingFromParent {
@@ -115,6 +117,9 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             self.timeoutCC.invalidate()
             UserDefaults.standard.removeObject(forKey: "inEditorPersonal")
             NotificationCenter.default.removeObserver(self)
+            super.viewDidDisappear(true)
+            self.removeFromParent()
+            self.dismiss(animated: true, completion: nil)
         }
     }
     
@@ -808,6 +813,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             titleNavigation.textColor = .white
             titleNavigation.font = UIFont.systemFont(ofSize: 12).bold
             navigationItem.titleView = viewAppBar
+            titleText = titleNavigation.text
         } else {
             searchBar = UISearchBar()
             searchBar.autocapitalizationType = .none
@@ -1023,6 +1029,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         }
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
+                var tempImages: [ImageGrouping] = []
                 while cursorData.next() {
                     var row: [String: Any?] = [:]
                     row["message_id"] = cursorData.string(forColumnIndex: 0)
@@ -1117,8 +1124,25 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                             }
                         }
                     }
+                    if row["image_id"] != nil && !(row["image_id"] as! String).isEmpty && (row["message_text"] as! String).isEmpty && (row["reff_id"] as! String).isEmpty && (row["credential"] as! String) != "1" && (row["read_receipts"] as! String) != "8" {
+                        tempImages.append(ImageGrouping(messageId: row["message_id"] as! String, thumbId: row["thumb_id"] as! String, imageId: row["image_id"] as! String, status: row["status"] as! String, time: row["server_date"] as! String))
+                    } else if tempImages.count >= 4 {
+                        groupImages[tempImages[0].messageId] = tempImages
+                        for _ in 1..<tempImages.count {
+                            dataMessages.removeLast()
+                        }
+                        tempImages.removeAll()
+                    } else if tempImages.count > 0 {
+                        tempImages.removeAll()
+                    }
                     dataMessages.append(row)
                 }
+                if tempImages.count >= 4 {
+                    groupImages[tempImages[0].messageId] = tempImages
+                    for _ in 1..<tempImages.count {
+                        dataMessages.removeLast()
+                    }
+                }
                 cursorData.close()
             }
         })
@@ -4283,6 +4307,7 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
             return cell
         }
         
+        let messageIdChat = (dataMessages[indexPath.row]["message_id"] as? String) ?? ""
         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) ?? ""
@@ -4755,102 +4780,210 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
         let containerViewFile = UIView()
         
         if (!thumbChat.isEmpty && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as! String != "1") && (dataMessages[indexPath.row]["lock"] as? String != "2")) {
-            topMarginText.constant = topMarginText.constant + 205
-            
-            containerMessage.addSubview(imageThumb)
-            imageThumb.translatesAutoresizingMaskIntoConstraints = false
-            let data = queryMessageReply(message_id: reffChat)
-            if (data.count == 0) {
-                imageThumb.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 15).isActive = true
-            }
-            imageThumb.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
-            imageThumb.bottomAnchor.constraint(equalTo: messageText.topAnchor, constant: -5).isActive = true
-            imageThumb.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
-            imageThumb.widthAnchor.constraint(equalToConstant: self.view.frame.size.width * 0.6).isActive = true
-            imageThumb.layer.cornerRadius = 5.0
-            imageThumb.clipsToBounds = true
-            imageThumb.contentMode = .scaleAspectFill
-            
-            let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
-            let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
-            let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
-            if let dirPath = paths.first {
-                let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumbChat)
-                let image    = UIImage(contentsOfFile: thumbURL.path)
-                imageThumb.image = image
-                
-                let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(videoChat)
-                let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(imageChat)
-                if !FileManager.default.fileExists(atPath: imageURL.path) || !FileManager.default.fileExists(atPath: videoURL.path) {
-                    let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.light)
+            if let listImages = groupImages[messageIdChat] {
+                timeMessage.isHidden = true
+                statusMessage.isHidden = true
+                topMarginText.constant = topMarginText.constant + 225
+                let listImageThumb: [UIImageView] = [UIImageView(), UIImageView(), UIImageView(), UIImageView()]
+                for i in 0..<4 {
+                    containerMessage.addSubview(listImageThumb[i])
+                    listImageThumb[i].layer.cornerRadius = 5.0
+                    listImageThumb[i].clipsToBounds = true
+                    listImageThumb[i].contentMode = .scaleAspectFill
+                    let widthHeightImage: CGFloat = 120
+                    switch i {
+                        case 0:
+                            listImageThumb[i].anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, paddingTop: 5, paddingLeft: 5, width: widthHeightImage, height: widthHeightImage)
+                        case 1:
+                            listImageThumb[i].anchor(top: containerMessage.topAnchor, left: listImageThumb[0].rightAnchor, right: containerMessage.rightAnchor, paddingTop: 5, paddingLeft: 5, paddingRight: 5, width: widthHeightImage, height: widthHeightImage)
+                        case 2:
+                            listImageThumb[i].anchor(left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, paddingLeft: 5, paddingBottom: 5, width: widthHeightImage, height: widthHeightImage)
+                        default:
+                            listImageThumb[i].anchor(left: listImageThumb[2].rightAnchor, bottom: containerMessage.bottomAnchor, right: containerMessage.rightAnchor, paddingLeft: 5, paddingBottom: 5, paddingRight: 5, width: widthHeightImage, height: widthHeightImage)
+                    }
+                    let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
+                    let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
+                    let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
+                    if let dirPath = paths.first {
+                        let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(listImages[i].thumbId)
+//                        let image    = UIImage(contentsOfFile: thumbURL.path)
+                        let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
+                        listImageThumb[i].image = image
+
+                        let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(listImages[i].imageId)
+                        if !FileManager.default.fileExists(atPath: imageURL.path) {
+                            let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.light)
+                            let blurEffectView = UIVisualEffectView(effect: blurEffect)
+                            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)
+                            if !listImages[i].imageId.isEmpty {
+                                let imageDownload = UIImageView(image: UIImage(systemName: "arrow.down.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 50, weight: .bold, scale: .default)))
+                                listImageThumb[i].addSubview(imageDownload)
+                                imageDownload.tintColor = .black.withAlphaComponent(0.3)
+                                imageDownload.translatesAutoresizingMaskIntoConstraints = false
+                                imageDownload.centerXAnchor.constraint(equalTo: listImageThumb[i].centerXAnchor).isActive = true
+                                imageDownload.centerYAnchor.constraint(equalTo: listImageThumb[i].centerYAnchor).isActive = true
+                            }
+                        }
+
+                    }
+                    let containerTimeStatus = UIView()
+                    listImageThumb[i].addSubview(containerTimeStatus)
+                    containerTimeStatus.anchor(bottom: listImageThumb[i].bottomAnchor, right: listImageThumb[i].rightAnchor, height: 15)
+                    let widthcontainerTimeStatus = containerTimeStatus.widthAnchor.constraint(equalToConstant: 50)
+                    widthcontainerTimeStatus.isActive = true
+                    containerTimeStatus.layer.cornerRadius = 5.0
+                    containerTimeStatus.layer.masksToBounds = true
+                    containerTimeStatus.backgroundColor = .black.withAlphaComponent(0.15)
+                    
+                    let timeInImage = UILabel()
+                    containerTimeStatus.addSubview(timeInImage)
+                    let date = Date(milliseconds: Int64(listImages[i].time) ?? 100)
+                    let formatter = DateFormatter()
+                    formatter.dateFormat = "HH:mm"
+                    formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
+                    timeInImage.text = formatter.string(from: date as Date)
+                    timeInImage.textColor = .white
+                    timeInImage.font = UIFont.systemFont(ofSize: 10, weight: .medium)
+                    
+                    if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+                        let statusInImage = UIImageView()
+                        containerTimeStatus.addSubview(statusInImage)
+                        statusInImage.anchor(right: containerTimeStatus.rightAnchor, centerY: containerTimeStatus.centerYAnchor, width: 15, height: 15)
+                        if listImages[i].status == "1" || listImages[i].status == "2"  {
+                            statusInImage.image = UIImage(named: "checklist", in: Bundle.resourceBundle(for: DigiX.self), with: nil)!.withTintColor(UIColor.white)
+                        } else if listImages[i].status == "3" {
+                            statusInImage.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: DigiX.self), with: nil)!.withTintColor(UIColor.white)
+                        } else {
+                            statusInImage.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: DigiX.self), with: nil)!.withTintColor(UIColor.systemBlue)
+                        }
+                        timeInImage.anchor(right: statusInImage.leftAnchor, centerY: containerTimeStatus.centerYAnchor, height: 15)
+                    } else {
+                        timeInImage.anchor(right: containerTimeStatus.rightAnchor, paddingRight: 5, centerY: containerTimeStatus.centerYAnchor, height: 15)
+                        widthcontainerTimeStatus.constant = 40
+                    }
+                    if !copySession && !forwardSession && !deleteSession {
+                        let objectTap = ObjectGesture(target: self, action: #selector(imageGroupingTapped(_:)))
+                        listImageThumb[i].isUserInteractionEnabled = true
+                        listImageThumb[i].addGestureRecognizer(objectTap)
+                        objectTap.indexImageTapped = i
+                        objectTap.listImageFromGrouping = listImages
+                    }
+                }
+                if  listImages.count > 4 {
+                    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.frame = CGRect(x: 0, y: 0, width: listImageThumb[3].frame.size.width, height: listImageThumb[3].frame.size.height)
                     blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
-                    imageThumb.addSubview(blurEffectView)
-                    if !imageChat.isEmpty {
-                        let imageDownload = UIImageView(image: UIImage(systemName: "arrow.down.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 50, weight: .bold, scale: .default)))
-                        imageThumb.addSubview(imageDownload)
-                        imageDownload.tintColor = .black.withAlphaComponent(0.3)
-                        imageDownload.translatesAutoresizingMaskIntoConstraints = false
-                        imageDownload.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
-                        imageDownload.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
+                    listImageThumb[3].addSubview(blurEffectView)
+                    
+                    let countRestImages = UILabel()
+                    listImageThumb[3].addSubview(countRestImages)
+                    countRestImages.anchor(centerX: listImageThumb[3].centerXAnchor, centerY: listImageThumb[3].centerYAnchor)
+                    countRestImages.font = UIFont.systemFont(ofSize: 30, weight: .medium)
+                    countRestImages.text = "+\(listImages.count - 3)"
+                    countRestImages.textColor = .white
+                }
+            } else {
+                topMarginText.constant = topMarginText.constant + 205
+                
+                containerMessage.addSubview(imageThumb)
+                imageThumb.translatesAutoresizingMaskIntoConstraints = false
+                let data = queryMessageReply(message_id: reffChat)
+                if (data.count == 0) {
+                    imageThumb.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 15).isActive = true
+                }
+                imageThumb.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
+                imageThumb.bottomAnchor.constraint(equalTo: messageText.topAnchor, constant: -5).isActive = true
+                imageThumb.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
+                imageThumb.widthAnchor.constraint(equalToConstant: self.view.frame.size.width * 0.6).isActive = true
+                imageThumb.layer.cornerRadius = 5.0
+                imageThumb.clipsToBounds = true
+                imageThumb.contentMode = .scaleAspectFill
+                
+                let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
+                let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
+                let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
+                if let dirPath = paths.first {
+                    let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumbChat)
+//                    let image    = UIImage(contentsOfFile: thumbURL.path)
+                    let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
+                    imageThumb.image = image
+                    
+                    let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(videoChat)
+                    let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(imageChat)
+                    if !FileManager.default.fileExists(atPath: imageURL.path) || !FileManager.default.fileExists(atPath: videoURL.path) {
+                        let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.light)
+                        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)
+                        if !imageChat.isEmpty {
+                            let imageDownload = UIImageView(image: UIImage(systemName: "arrow.down.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 50, weight: .bold, scale: .default)))
+                            imageThumb.addSubview(imageDownload)
+                            imageDownload.tintColor = .black.withAlphaComponent(0.3)
+                            imageDownload.translatesAutoresizingMaskIntoConstraints = false
+                            imageDownload.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
+                            imageDownload.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
+                        }
                     }
+                    
                 }
                 
-            }
-            
-            if (videoChat != "") {
-                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()
-                imageThumb.addSubview(imagePlay)
-                imagePlay.backgroundColor = .black.withAlphaComponent(0.3)
-                imagePlay.translatesAutoresizingMaskIntoConstraints = false
-                imagePlay.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
-                imagePlay.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
-            }
-            
-            if (dataMessages[indexPath.row]["progress"] as! Double != 100.0 && dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
-                let container = UIView()
-                imageThumb.addSubview(container)
-                container.translatesAutoresizingMaskIntoConstraints = false
-                container.bottomAnchor.constraint(equalTo: imageThumb.bottomAnchor, constant: -10).isActive = true
-                container.leadingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: 10).isActive = true
-                container.widthAnchor.constraint(equalToConstant: 30).isActive = true
-                container.heightAnchor.constraint(equalToConstant: 30).isActive = true
-                container.backgroundColor = .white.withAlphaComponent(0.1)
-                let circlePath = UIBezierPath(arcCenter: CGPoint(x: 10, y: 20), radius: 15, startAngle: -(.pi / 2), endAngle: .pi * 2, clockwise: true)
-                let trackShape = CAShapeLayer()
-                trackShape.path = circlePath.cgPath
-                trackShape.fillColor = UIColor.black.withAlphaComponent(0.3).cgColor
-                trackShape.lineWidth = 3
-                trackShape.strokeColor = UIColor.blueBubbleColor.withAlphaComponent(0.3).cgColor
-                container.backgroundColor = .clear
-                container.layer.addSublayer(trackShape)
-                let shapeLoading = CAShapeLayer()
-                shapeLoading.path = circlePath.cgPath
-                shapeLoading.fillColor = UIColor.clear.cgColor
-                shapeLoading.lineWidth = 3
-                shapeLoading.strokeEnd = 0
-                shapeLoading.strokeColor = UIColor.blueBubbleColor.cgColor
-                container.layer.addSublayer(shapeLoading)
-                let imageupload = UIImageView(image: UIImage(systemName: "arrow.up", withConfiguration: UIImage.SymbolConfiguration(pointSize: 10, weight: .bold, scale: .default)))
-                imageupload.tintColor = .white
-                container.addSubview(imageupload)
-                imageupload.translatesAutoresizingMaskIntoConstraints = false
-                imageupload.bottomAnchor.constraint(equalTo: imageThumb.bottomAnchor, constant: -10).isActive = true
-                imageupload.leadingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: 10).isActive = true
-                imageupload.widthAnchor.constraint(equalToConstant: 20).isActive = true
-                imageupload.heightAnchor.constraint(equalToConstant: 20).isActive = true
-            }
-            
-            if !copySession && !forwardSession && !deleteSession {
-                let objectTap = ObjectGesture(target: self, action: #selector(contentMessageTapped(_:)))
-                imageThumb.isUserInteractionEnabled = true
-                imageThumb.addGestureRecognizer(objectTap)
-                objectTap.image_id = imageChat
-                objectTap.video_id = videoChat
-                objectTap.imageView = imageThumb
-                objectTap.indexPath = indexPath
+                if (videoChat != "") {
+                    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()
+                    imageThumb.addSubview(imagePlay)
+                    imagePlay.backgroundColor = .black.withAlphaComponent(0.3)
+                    imagePlay.translatesAutoresizingMaskIntoConstraints = false
+                    imagePlay.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
+                    imagePlay.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
+                }
+                
+                if (dataMessages[indexPath.row]["progress"] as! Double != 100.0 && dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+                    let container = UIView()
+                    imageThumb.addSubview(container)
+                    container.translatesAutoresizingMaskIntoConstraints = false
+                    container.bottomAnchor.constraint(equalTo: imageThumb.bottomAnchor, constant: -10).isActive = true
+                    container.leadingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: 10).isActive = true
+                    container.widthAnchor.constraint(equalToConstant: 30).isActive = true
+                    container.heightAnchor.constraint(equalToConstant: 30).isActive = true
+                    container.backgroundColor = .white.withAlphaComponent(0.1)
+                    let circlePath = UIBezierPath(arcCenter: CGPoint(x: 10, y: 20), radius: 15, startAngle: -(.pi / 2), endAngle: .pi * 2, clockwise: true)
+                    let trackShape = CAShapeLayer()
+                    trackShape.path = circlePath.cgPath
+                    trackShape.fillColor = UIColor.black.withAlphaComponent(0.3).cgColor
+                    trackShape.lineWidth = 3
+                    trackShape.strokeColor = UIColor.blueBubbleColor.withAlphaComponent(0.3).cgColor
+                    container.backgroundColor = .clear
+                    container.layer.addSublayer(trackShape)
+                    let shapeLoading = CAShapeLayer()
+                    shapeLoading.path = circlePath.cgPath
+                    shapeLoading.fillColor = UIColor.clear.cgColor
+                    shapeLoading.lineWidth = 3
+                    shapeLoading.strokeEnd = 0
+                    shapeLoading.strokeColor = UIColor.blueBubbleColor.cgColor
+                    container.layer.addSublayer(shapeLoading)
+                    let imageupload = UIImageView(image: UIImage(systemName: "arrow.up", withConfiguration: UIImage.SymbolConfiguration(pointSize: 10, weight: .bold, scale: .default)))
+                    imageupload.tintColor = .white
+                    container.addSubview(imageupload)
+                    imageupload.translatesAutoresizingMaskIntoConstraints = false
+                    imageupload.bottomAnchor.constraint(equalTo: imageThumb.bottomAnchor, constant: -10).isActive = true
+                    imageupload.leadingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: 10).isActive = true
+                    imageupload.widthAnchor.constraint(equalToConstant: 20).isActive = true
+                    imageupload.heightAnchor.constraint(equalToConstant: 20).isActive = true
+                }
+                
+                if !copySession && !forwardSession && !deleteSession {
+                    let objectTap = ObjectGesture(target: self, action: #selector(contentMessageTapped(_:)))
+                    imageThumb.isUserInteractionEnabled = true
+                    imageThumb.addGestureRecognizer(objectTap)
+                    objectTap.image_id = imageChat
+                    objectTap.video_id = videoChat
+                    objectTap.imageView = imageThumb
+                    objectTap.indexPath = indexPath
+                }
             }
         }
         
@@ -5190,7 +5323,8 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
                     let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
                     if let dirPath = paths.first {
                         let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumb_chat)
-                        let image    = UIImage(contentsOfFile: thumbURL.path)
+//                        let image    = UIImage(contentsOfFile: thumbURL.path)
+                        let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
                         let imageThumb = UIImageView(image: image)
                         containerReply.addSubview(imageThumb)
                         imageThumb.layer.cornerRadius = 2.0
@@ -5244,6 +5378,14 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
         return cell
     }
     
+    @objc func imageGroupingTapped(_ sender: ObjectGesture) {
+        let listGroupingImages = ListGroupImages()
+        listGroupingImages.imageTapped = sender.indexImageTapped
+        listGroupingImages.listGroupingImages = sender.listImageFromGrouping
+        listGroupingImages.titleName = titleText
+        self.navigationController?.pushViewController(listGroupingImages, animated: true)
+    }
+    
     @objc func tapAck(_ sender: ObjectGesture) {
         if blocking == "1" {
             self.view.makeToast("You blocked this user".localized())
@@ -5824,7 +5966,8 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
             let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
             if let dirPath = paths.first {
                 let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumb_chat)
-                let image    = UIImage(contentsOfFile: thumbURL.path)
+//                let image    = UIImage(contentsOfFile: thumbURL.path)
+                let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
                 let imageThumb = UIImageView(image: image)
                 self.containerPreviewReply.addSubview(imageThumb)
                 imageThumb.layer.cornerRadius = 2.0
@@ -6030,6 +6173,8 @@ public class ObjectGesture: UITapGestureRecognizer {
     public var labelFile = UILabel()
     public var videoURL: NSURL?
     public var indexPath = IndexPath()
+    public var indexImageTapped: Int!
+    public var listImageFromGrouping: [ImageGrouping]!
 }
 
 class navigationQLPreviewDocument: UIBarButtonItem {
@@ -6039,3 +6184,19 @@ class navigationQLPreviewDocument: UIBarButtonItem {
 class segmentedControllerObject: UISegmentedControl {
     var navigation = UINavigationController()
 }
+
+public class ImageGrouping {
+    public var messageId = ""
+    public var thumbId = ""
+    public var imageId = ""
+    public var status = ""
+    public var time = ""
+    
+    public init(messageId: String, thumbId: String, imageId: String, status: String, time: String) {
+        self.messageId = messageId
+        self.thumbId = thumbId
+        self.imageId = imageId
+        self.status = status
+        self.time = time
+    }
+}

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

@@ -98,10 +98,10 @@ public class EditorGroup: UIViewController {
         if self.isMovingFromParent {
             UserDefaults.standard.removeObject(forKey: "inEditorGroup")
             NotificationCenter.default.removeObserver(self)
+            super.viewDidDisappear(true)
+            self.removeFromParent()
+            self.dismiss(animated: true, completion: nil)
         }
-        super.viewDidDisappear(true)
-        self.removeFromParent()
-        self.dismiss(animated: true, completion: nil)
     }
     
     public override func viewDidAppear(_ animated: Bool) {

+ 250 - 95
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -106,6 +106,8 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     var isDirectCC = false
     var fakeProgMultip = 0
     let maxFakeProgMultip = 2
+    var groupImages: [String:[ImageGrouping]] = [:]
+    var titleText: String!
     
     public override func viewDidDisappear(_ animated: Bool) {
         if self.isMovingFromParent {
@@ -115,10 +117,10 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             self.timeoutCC.invalidate()
             UserDefaults.standard.removeObject(forKey: "inEditorPersonal")
             NotificationCenter.default.removeObserver(self)
+            super.viewDidDisappear(true)
+            self.removeFromParent()
+            self.dismiss(animated: true, completion: nil)
         }
-        super.viewDidDisappear(true)
-        self.removeFromParent()
-        self.dismiss(animated: true, completion: nil)
     }
     
     public override func viewDidAppear(_ animated: Bool) {
@@ -812,6 +814,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             titleNavigation.textColor = .white
             titleNavigation.font = UIFont.systemFont(ofSize: 12).bold
             navigationItem.titleView = viewAppBar
+            titleText = titleNavigation.text
         } else {
             searchBar = UISearchBar()
             searchBar.autocapitalizationType = .none
@@ -1027,6 +1030,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         }
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
+                var tempImages: [ImageGrouping] = []
                 while cursorData.next() {
                     var row: [String: Any?] = [:]
                     row["message_id"] = cursorData.string(forColumnIndex: 0)
@@ -1121,8 +1125,25 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                             }
                         }
                     }
+                    if row["image_id"] != nil && !(row["image_id"] as! String).isEmpty && (row["message_text"] as! String).isEmpty && (row["reff_id"] as! String).isEmpty && (row["credential"] as! String) != "1" && (row["read_receipts"] as! String) != "8" {
+                        tempImages.append(ImageGrouping(messageId: row["message_id"] as! String, thumbId: row["thumb_id"] as! String, imageId: row["image_id"] as! String, status: row["status"] as! String, time: row["server_date"] as! String))
+                    } else if tempImages.count >= 4 {
+                        groupImages[tempImages[0].messageId] = tempImages
+                        for _ in 1..<tempImages.count {
+                            dataMessages.removeLast()
+                        }
+                        tempImages.removeAll()
+                    } else if tempImages.count > 0 {
+                        tempImages.removeAll()
+                    }
                     dataMessages.append(row)
                 }
+                if tempImages.count >= 4 {
+                    groupImages[tempImages[0].messageId] = tempImages
+                    for _ in 1..<tempImages.count {
+                        dataMessages.removeLast()
+                    }
+                }
                 cursorData.close()
             }
         })
@@ -4296,6 +4317,7 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
             return cell
         }
         
+        let messageIdChat = (dataMessages[indexPath.row]["message_id"] as? String) ?? ""
         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) ?? ""
@@ -4768,103 +4790,210 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
         let containerViewFile = UIView()
         
         if (!thumbChat.isEmpty && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as! String != "1") && (dataMessages[indexPath.row]["lock"] as? String != "2")) {
-            topMarginText.constant = topMarginText.constant + 205
-            
-            containerMessage.addSubview(imageThumb)
-            imageThumb.translatesAutoresizingMaskIntoConstraints = false
-            let data = queryMessageReply(message_id: reffChat)
-            if (data.count == 0) {
-                imageThumb.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 15).isActive = true
-            }
-            imageThumb.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
-            imageThumb.bottomAnchor.constraint(equalTo: messageText.topAnchor, constant: -5).isActive = true
-            imageThumb.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
-            imageThumb.widthAnchor.constraint(equalToConstant: self.view.frame.size.width * 0.6).isActive = true
-            imageThumb.layer.cornerRadius = 5.0
-            imageThumb.clipsToBounds = true
-            imageThumb.contentMode = .scaleAspectFill
-            
-            let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
-            let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
-            let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
-            if let dirPath = paths.first {
-                let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumbChat)
-//                let image    = UIImage(contentsOfFile: thumbURL.path)
-                let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
-                imageThumb.image = image
-                
-                let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(videoChat)
-                let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(imageChat)
-                if !FileManager.default.fileExists(atPath: imageURL.path) || !FileManager.default.fileExists(atPath: videoURL.path) {
-                    let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.light)
+            if let listImages = groupImages[messageIdChat] {
+                timeMessage.isHidden = true
+                statusMessage.isHidden = true
+                topMarginText.constant = topMarginText.constant + 225
+                let listImageThumb: [UIImageView] = [UIImageView(), UIImageView(), UIImageView(), UIImageView()]
+                for i in 0..<4 {
+                    containerMessage.addSubview(listImageThumb[i])
+                    listImageThumb[i].layer.cornerRadius = 5.0
+                    listImageThumb[i].clipsToBounds = true
+                    listImageThumb[i].contentMode = .scaleAspectFill
+                    let widthHeightImage: CGFloat = 120
+                    switch i {
+                        case 0:
+                            listImageThumb[i].anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, paddingTop: 5, paddingLeft: 5, width: widthHeightImage, height: widthHeightImage)
+                        case 1:
+                            listImageThumb[i].anchor(top: containerMessage.topAnchor, left: listImageThumb[0].rightAnchor, right: containerMessage.rightAnchor, paddingTop: 5, paddingLeft: 5, paddingRight: 5, width: widthHeightImage, height: widthHeightImage)
+                        case 2:
+                            listImageThumb[i].anchor(left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, paddingLeft: 5, paddingBottom: 5, width: widthHeightImage, height: widthHeightImage)
+                        default:
+                            listImageThumb[i].anchor(left: listImageThumb[2].rightAnchor, bottom: containerMessage.bottomAnchor, right: containerMessage.rightAnchor, paddingLeft: 5, paddingBottom: 5, paddingRight: 5, width: widthHeightImage, height: widthHeightImage)
+                    }
+                    let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
+                    let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
+                    let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
+                    if let dirPath = paths.first {
+                        let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(listImages[i].thumbId)
+//                        let image    = UIImage(contentsOfFile: thumbURL.path)
+                        let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
+                        listImageThumb[i].image = image
+
+                        let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(listImages[i].imageId)
+                        if !FileManager.default.fileExists(atPath: imageURL.path) {
+                            let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.light)
+                            let blurEffectView = UIVisualEffectView(effect: blurEffect)
+                            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)
+                            if !listImages[i].imageId.isEmpty {
+                                let imageDownload = UIImageView(image: UIImage(systemName: "arrow.down.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 50, weight: .bold, scale: .default)))
+                                listImageThumb[i].addSubview(imageDownload)
+                                imageDownload.tintColor = .black.withAlphaComponent(0.3)
+                                imageDownload.translatesAutoresizingMaskIntoConstraints = false
+                                imageDownload.centerXAnchor.constraint(equalTo: listImageThumb[i].centerXAnchor).isActive = true
+                                imageDownload.centerYAnchor.constraint(equalTo: listImageThumb[i].centerYAnchor).isActive = true
+                            }
+                        }
+
+                    }
+                    let containerTimeStatus = UIView()
+                    listImageThumb[i].addSubview(containerTimeStatus)
+                    containerTimeStatus.anchor(bottom: listImageThumb[i].bottomAnchor, right: listImageThumb[i].rightAnchor, height: 15)
+                    let widthcontainerTimeStatus = containerTimeStatus.widthAnchor.constraint(equalToConstant: 50)
+                    widthcontainerTimeStatus.isActive = true
+                    containerTimeStatus.layer.cornerRadius = 5.0
+                    containerTimeStatus.layer.masksToBounds = true
+                    containerTimeStatus.backgroundColor = .black.withAlphaComponent(0.15)
+                    
+                    let timeInImage = UILabel()
+                    containerTimeStatus.addSubview(timeInImage)
+                    let date = Date(milliseconds: Int64(listImages[i].time) ?? 100)
+                    let formatter = DateFormatter()
+                    formatter.dateFormat = "HH:mm"
+                    formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
+                    timeInImage.text = formatter.string(from: date as Date)
+                    timeInImage.textColor = .white
+                    timeInImage.font = UIFont.systemFont(ofSize: 10, weight: .medium)
+                    
+                    if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+                        let statusInImage = UIImageView()
+                        containerTimeStatus.addSubview(statusInImage)
+                        statusInImage.anchor(right: containerTimeStatus.rightAnchor, centerY: containerTimeStatus.centerYAnchor, width: 15, height: 15)
+                        if listImages[i].status == "1" || listImages[i].status == "2"  {
+                            statusInImage.image = UIImage(named: "checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.white)
+                        } else if listImages[i].status == "3" {
+                            statusInImage.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.white)
+                        } else {
+                            statusInImage.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.systemBlue)
+                        }
+                        timeInImage.anchor(right: statusInImage.leftAnchor, centerY: containerTimeStatus.centerYAnchor, height: 15)
+                    } else {
+                        timeInImage.anchor(right: containerTimeStatus.rightAnchor, paddingRight: 5, centerY: containerTimeStatus.centerYAnchor, height: 15)
+                        widthcontainerTimeStatus.constant = 40
+                    }
+                    if !copySession && !forwardSession && !deleteSession {
+                        let objectTap = ObjectGesture(target: self, action: #selector(imageGroupingTapped(_:)))
+                        listImageThumb[i].isUserInteractionEnabled = true
+                        listImageThumb[i].addGestureRecognizer(objectTap)
+                        objectTap.indexImageTapped = i
+                        objectTap.listImageFromGrouping = listImages
+                    }
+                }
+                if  listImages.count > 4 {
+                    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.frame = CGRect(x: 0, y: 0, width: listImageThumb[3].frame.size.width, height: listImageThumb[3].frame.size.height)
                     blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
-                    imageThumb.addSubview(blurEffectView)
-                    if !imageChat.isEmpty {
-                        let imageDownload = UIImageView(image: UIImage(systemName: "arrow.down.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 50, weight: .bold, scale: .default)))
-                        imageThumb.addSubview(imageDownload)
-                        imageDownload.tintColor = .black.withAlphaComponent(0.3)
-                        imageDownload.translatesAutoresizingMaskIntoConstraints = false
-                        imageDownload.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
-                        imageDownload.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
+                    listImageThumb[3].addSubview(blurEffectView)
+                    
+                    let countRestImages = UILabel()
+                    listImageThumb[3].addSubview(countRestImages)
+                    countRestImages.anchor(centerX: listImageThumb[3].centerXAnchor, centerY: listImageThumb[3].centerYAnchor)
+                    countRestImages.font = UIFont.systemFont(ofSize: 30, weight: .medium)
+                    countRestImages.text = "+\(listImages.count - 3)"
+                    countRestImages.textColor = .white
+                }
+            } else {
+                topMarginText.constant = topMarginText.constant + 205
+                
+                containerMessage.addSubview(imageThumb)
+                imageThumb.translatesAutoresizingMaskIntoConstraints = false
+                let data = queryMessageReply(message_id: reffChat)
+                if (data.count == 0) {
+                    imageThumb.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 15).isActive = true
+                }
+                imageThumb.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
+                imageThumb.bottomAnchor.constraint(equalTo: messageText.topAnchor, constant: -5).isActive = true
+                imageThumb.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
+                imageThumb.widthAnchor.constraint(equalToConstant: self.view.frame.size.width * 0.6).isActive = true
+                imageThumb.layer.cornerRadius = 5.0
+                imageThumb.clipsToBounds = true
+                imageThumb.contentMode = .scaleAspectFill
+                
+                let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
+                let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
+                let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
+                if let dirPath = paths.first {
+                    let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumbChat)
+    //                let image    = UIImage(contentsOfFile: thumbURL.path)
+                    let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
+                    imageThumb.image = image
+                    
+                    let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(videoChat)
+                    let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(imageChat)
+                    if !FileManager.default.fileExists(atPath: imageURL.path) || !FileManager.default.fileExists(atPath: videoURL.path) {
+                        let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.light)
+                        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)
+                        if !imageChat.isEmpty {
+                            let imageDownload = UIImageView(image: UIImage(systemName: "arrow.down.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 50, weight: .bold, scale: .default)))
+                            imageThumb.addSubview(imageDownload)
+                            imageDownload.tintColor = .black.withAlphaComponent(0.3)
+                            imageDownload.translatesAutoresizingMaskIntoConstraints = false
+                            imageDownload.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
+                            imageDownload.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
+                        }
                     }
+                    
                 }
                 
-            }
-            
-            if (videoChat != "") {
-                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()
-                imageThumb.addSubview(imagePlay)
-                imagePlay.backgroundColor = .black.withAlphaComponent(0.3)
-                imagePlay.translatesAutoresizingMaskIntoConstraints = false
-                imagePlay.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
-                imagePlay.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
-            }
-            
-            if (dataMessages[indexPath.row]["progress"] as! Double != 100.0 && dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
-                let container = UIView()
-                imageThumb.addSubview(container)
-                container.translatesAutoresizingMaskIntoConstraints = false
-                container.bottomAnchor.constraint(equalTo: imageThumb.bottomAnchor, constant: -10).isActive = true
-                container.leadingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: 10).isActive = true
-                container.widthAnchor.constraint(equalToConstant: 30).isActive = true
-                container.heightAnchor.constraint(equalToConstant: 30).isActive = true
-                container.backgroundColor = .white.withAlphaComponent(0.1)
-                let circlePath = UIBezierPath(arcCenter: CGPoint(x: 10, y: 20), radius: 15, startAngle: -(.pi / 2), endAngle: .pi * 2, clockwise: true)
-                let trackShape = CAShapeLayer()
-                trackShape.path = circlePath.cgPath
-                trackShape.fillColor = UIColor.black.withAlphaComponent(0.3).cgColor
-                trackShape.lineWidth = 3
-                trackShape.strokeColor = UIColor.blueBubbleColor.withAlphaComponent(0.3).cgColor
-                container.backgroundColor = .clear
-                container.layer.addSublayer(trackShape)
-                let shapeLoading = CAShapeLayer()
-                shapeLoading.path = circlePath.cgPath
-                shapeLoading.fillColor = UIColor.clear.cgColor
-                shapeLoading.lineWidth = 3
-                shapeLoading.strokeEnd = 0
-                shapeLoading.strokeColor = UIColor.blueBubbleColor.cgColor
-                container.layer.addSublayer(shapeLoading)
-                let imageupload = UIImageView(image: UIImage(systemName: "arrow.up", withConfiguration: UIImage.SymbolConfiguration(pointSize: 10, weight: .bold, scale: .default)))
-                imageupload.tintColor = .white
-                container.addSubview(imageupload)
-                imageupload.translatesAutoresizingMaskIntoConstraints = false
-                imageupload.bottomAnchor.constraint(equalTo: imageThumb.bottomAnchor, constant: -10).isActive = true
-                imageupload.leadingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: 10).isActive = true
-                imageupload.widthAnchor.constraint(equalToConstant: 20).isActive = true
-                imageupload.heightAnchor.constraint(equalToConstant: 20).isActive = true
-            }
-            
-            if !copySession && !forwardSession && !deleteSession {
-                let objectTap = ObjectGesture(target: self, action: #selector(contentMessageTapped(_:)))
-                imageThumb.isUserInteractionEnabled = true
-                imageThumb.addGestureRecognizer(objectTap)
-                objectTap.image_id = imageChat
-                objectTap.video_id = videoChat
-                objectTap.imageView = imageThumb
-                objectTap.indexPath = indexPath
+                if (videoChat != "") {
+                    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()
+                    imageThumb.addSubview(imagePlay)
+                    imagePlay.backgroundColor = .black.withAlphaComponent(0.3)
+                    imagePlay.translatesAutoresizingMaskIntoConstraints = false
+                    imagePlay.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
+                    imagePlay.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
+                }
+                
+                if (dataMessages[indexPath.row]["progress"] as! Double != 100.0 && dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
+                    let container = UIView()
+                    imageThumb.addSubview(container)
+                    container.translatesAutoresizingMaskIntoConstraints = false
+                    container.bottomAnchor.constraint(equalTo: imageThumb.bottomAnchor, constant: -10).isActive = true
+                    container.leadingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: 10).isActive = true
+                    container.widthAnchor.constraint(equalToConstant: 30).isActive = true
+                    container.heightAnchor.constraint(equalToConstant: 30).isActive = true
+                    container.backgroundColor = .white.withAlphaComponent(0.1)
+                    let circlePath = UIBezierPath(arcCenter: CGPoint(x: 10, y: 20), radius: 15, startAngle: -(.pi / 2), endAngle: .pi * 2, clockwise: true)
+                    let trackShape = CAShapeLayer()
+                    trackShape.path = circlePath.cgPath
+                    trackShape.fillColor = UIColor.black.withAlphaComponent(0.3).cgColor
+                    trackShape.lineWidth = 3
+                    trackShape.strokeColor = UIColor.blueBubbleColor.withAlphaComponent(0.3).cgColor
+                    container.backgroundColor = .clear
+                    container.layer.addSublayer(trackShape)
+                    let shapeLoading = CAShapeLayer()
+                    shapeLoading.path = circlePath.cgPath
+                    shapeLoading.fillColor = UIColor.clear.cgColor
+                    shapeLoading.lineWidth = 3
+                    shapeLoading.strokeEnd = 0
+                    shapeLoading.strokeColor = UIColor.blueBubbleColor.cgColor
+                    container.layer.addSublayer(shapeLoading)
+                    let imageupload = UIImageView(image: UIImage(systemName: "arrow.up", withConfiguration: UIImage.SymbolConfiguration(pointSize: 10, weight: .bold, scale: .default)))
+                    imageupload.tintColor = .white
+                    container.addSubview(imageupload)
+                    imageupload.translatesAutoresizingMaskIntoConstraints = false
+                    imageupload.bottomAnchor.constraint(equalTo: imageThumb.bottomAnchor, constant: -10).isActive = true
+                    imageupload.leadingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: 10).isActive = true
+                    imageupload.widthAnchor.constraint(equalToConstant: 20).isActive = true
+                    imageupload.heightAnchor.constraint(equalToConstant: 20).isActive = true
+                }
+                
+                if !copySession && !forwardSession && !deleteSession {
+                    let objectTap = ObjectGesture(target: self, action: #selector(contentMessageTapped(_:)))
+                    imageThumb.isUserInteractionEnabled = true
+                    imageThumb.addGestureRecognizer(objectTap)
+                    objectTap.image_id = imageChat
+                    objectTap.video_id = videoChat
+                    objectTap.imageView = imageThumb
+                    objectTap.indexPath = indexPath
+                }
             }
         }
         
@@ -5259,6 +5388,14 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
         return cell
     }
     
+    @objc func imageGroupingTapped(_ sender: ObjectGesture) {
+        let listGroupingImages = ListGroupImages()
+        listGroupingImages.imageTapped = sender.indexImageTapped
+        listGroupingImages.listGroupingImages = sender.listImageFromGrouping
+        listGroupingImages.titleName = titleText
+        self.navigationController?.pushViewController(listGroupingImages, animated: true)
+    }
+    
     @objc func tapAck(_ sender: ObjectGesture) {
         if blocking == "1" {
             self.view.makeToast("You blocked this user".localized())
@@ -6047,6 +6184,8 @@ public class ObjectGesture: UITapGestureRecognizer {
     public var labelFile = UILabel()
     public var videoURL: NSURL?
     public var indexPath = IndexPath()
+    public var indexImageTapped: Int!
+    public var listImageFromGrouping: [ImageGrouping]!
 }
 
 class navigationQLPreviewDocument: UIBarButtonItem {
@@ -6056,3 +6195,19 @@ class navigationQLPreviewDocument: UIBarButtonItem {
 class segmentedControllerObject: UISegmentedControl {
     var navigation = UINavigationController()
 }
+
+public class ImageGrouping {
+    public var messageId = ""
+    public var thumbId = ""
+    public var imageId = ""
+    public var status = ""
+    public var time = ""
+    
+    public init(messageId: String, thumbId: String, imageId: String, status: String, time: String) {
+        self.messageId = messageId
+        self.thumbId = thumbId
+        self.imageId = imageId
+        self.status = status
+        self.time = time
+    }
+}

+ 69 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ListGroupImages.swift

@@ -0,0 +1,69 @@
+//
+//  ListGroupImages.swift
+//  NexilisLite
+//
+//  Created by Akhmad Al Qindi Irsyam on 28/07/23.
+//
+
+import UIKit
+
+class ListGroupImages: UIViewController {
+    var listGroupingImages: [ImageGrouping]!
+    var imageTapped: Int!
+    var titleName: String!
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        view.backgroundColor = .white
+
+        let centeredTitleView = CenteredTitleSubtitleView(frame: CGRect(x: 0, y: 0, width: 200, height: 44))
+        centeredTitleView.titleLabel.text = titleName
+        centeredTitleView.subtitleLabel.text = String(listGroupingImages.count) + " " + "images".localized()
+        navigationItem.titleView = centeredTitleView
+    }
+
+}
+
+class CenteredTitleSubtitleView: UIView {
+    let titleLabel: UILabel = {
+        let label = UILabel()
+        label.textAlignment = .center
+        label.font = UIFont.boldSystemFont(ofSize: 18)
+        label.textColor = .white
+        return label
+    }()
+    
+    let subtitleLabel: UILabel = {
+        let label = UILabel()
+        label.textAlignment = .center
+        label.font = UIFont.systemFont(ofSize: 14)
+        label.textColor = .lightGray
+        return label
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupSubviews()
+    }
+    
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        setupSubviews()
+    }
+    
+    private func setupSubviews() {
+        addSubview(titleLabel)
+        addSubview(subtitleLabel)
+        
+        // Add any constraints or frames you prefer
+        // Here's an example using autolayout anchors
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
+        titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
+        titleLabel.topAnchor.constraint(equalTo: topAnchor).isActive = true
+        
+        subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
+        subtitleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
+        subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true
+    }
+}