|  |  |  | // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import UIKit | 
					
						
							|  |  |  | import SessionUtilitiesKit | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | public extension UIContextualAction { | 
					
						
							|  |  |  |     private static var lookupMap: Atomic<[Int: [String: [Int: ThemeValue]]]> = Atomic([:]) | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     enum Side: Int { | 
					
						
							|  |  |  |         case leading | 
					
						
							|  |  |  |         case trailing | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         func key(for indexPath: IndexPath) -> String { | 
					
						
							|  |  |  |             return "\(indexPath.section)-\(indexPath.row)-\(rawValue)" | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         init?(for view: UIView) { | 
					
						
							|  |  |  |             guard view.frame.minX == 0 else { | 
					
						
							|  |  |  |                 self = .trailing | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             self = .leading | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     convenience init( | 
					
						
							|  |  |  |         title: String? = nil, | 
					
						
							|  |  |  |         icon: UIImage? = nil, | 
					
						
							|  |  |  |         iconHeight: CGFloat = Values.mediumFontSize, | 
					
						
							|  |  |  |         themeTintColor: ThemeValue = .white, | 
					
						
							|  |  |  |         themeBackgroundColor: ThemeValue, | 
					
						
							|  |  |  |         side: Side, | 
					
						
							|  |  |  |         actionIndex: Int, | 
					
						
							|  |  |  |         indexPath: IndexPath, | 
					
						
							|  |  |  |         tableView: UITableView, | 
					
						
							|  |  |  |         handler: @escaping UIContextualAction.Handler | 
					
						
							|  |  |  |     ) { | 
					
						
							|  |  |  |         self.init(style: .normal, title: title, handler: handler) | 
					
						
							|  |  |  |         self.image = UIContextualAction | 
					
						
							|  |  |  |             .imageWith( | 
					
						
							|  |  |  |                 title: title, | 
					
						
							|  |  |  |                 icon: icon, | 
					
						
							|  |  |  |                 iconHeight: iconHeight, | 
					
						
							|  |  |  |                 themeTintColor: themeTintColor | 
					
						
							|  |  |  |             )? | 
					
						
							|  |  |  |             .withRenderingMode(.alwaysTemplate) | 
					
						
							|  |  |  |         self.themeBackgroundColor = themeBackgroundColor | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         UIContextualAction.lookupMap.mutate { | 
					
						
							|  |  |  |             $0[tableView.hashValue] = ($0[tableView.hashValue] ?? [:]) | 
					
						
							|  |  |  |                 .setting( | 
					
						
							|  |  |  |                     side.key(for: indexPath), | 
					
						
							|  |  |  |                     (($0[tableView.hashValue] ?? [:])[side.key(for: indexPath)] ?? [:]) | 
					
						
							|  |  |  |                         .setting(actionIndex, themeTintColor) | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private static func imageWith( | 
					
						
							|  |  |  |         title: String?, | 
					
						
							|  |  |  |         icon: UIImage?, | 
					
						
							|  |  |  |         iconHeight: CGFloat, | 
					
						
							|  |  |  |         themeTintColor: ThemeValue | 
					
						
							|  |  |  |     ) -> UIImage? { | 
					
						
							|  |  |  |         let stackView: UIStackView = UIStackView() | 
					
						
							|  |  |  |         stackView.axis = .vertical | 
					
						
							|  |  |  |         stackView.alignment = .center | 
					
						
							|  |  |  |         stackView.spacing = 4 | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         if let icon: UIImage = icon { | 
					
						
							|  |  |  |             let aspectRatio: CGFloat = (icon.size.width / icon.size.height) | 
					
						
							|  |  |  |             let imageView: UIImageView = UIImageView(image: icon) | 
					
						
							|  |  |  |             imageView.frame = CGRect(x: 0, y: 0, width: (iconHeight * aspectRatio), height: iconHeight) | 
					
						
							|  |  |  |             imageView.contentMode = .scaleAspectFit | 
					
						
							|  |  |  |             imageView.themeTintColor = themeTintColor | 
					
						
							|  |  |  |             stackView.addArrangedSubview(imageView) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         if let title: String = title { | 
					
						
							|  |  |  |             let label: UILabel = UILabel() | 
					
						
							|  |  |  |             label.font = .systemFont(ofSize: Values.smallFontSize) | 
					
						
							|  |  |  |             label.text = title | 
					
						
							|  |  |  |             label.textAlignment = .center | 
					
						
							|  |  |  |             label.themeTextColor = themeTintColor | 
					
						
							|  |  |  |             label.minimumScaleFactor = 0.75 | 
					
						
							|  |  |  |             label.numberOfLines = (title.components(separatedBy: " ").count > 1 ? 2 : 1) | 
					
						
							|  |  |  |             label.frame = CGRect( | 
					
						
							|  |  |  |                 origin: .zero, | 
					
						
							|  |  |  |                 // Note: It looks like there is a semi-max width of 68px for images in the swipe actions | 
					
						
							|  |  |  |                 // if the image ends up larger then there an odd behaviour can occur where 8/10 times the | 
					
						
							|  |  |  |                 // image is scaled down to fit, but ocassionally (primarily if you hide the action and | 
					
						
							|  |  |  |                 // immediately swipe to show it again once the cell hits the edge of the screen) the image | 
					
						
							|  |  |  |                 // won't be scaled down but will be full size - appearing as if two different images are used | 
					
						
							|  |  |  |                 size: label.sizeThatFits(CGSize(width: 68, height: 999)) | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             label.set(.width, to: label.frame.width) | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             stackView.addArrangedSubview(label) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         stackView.frame = CGRect( | 
					
						
							|  |  |  |             origin: .zero, | 
					
						
							|  |  |  |             size: stackView.systemLayoutSizeFitting(CGSize(width: 999, height: 999)) | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Based on https://stackoverflow.com/a/41288197/1118398 | 
					
						
							|  |  |  |         let renderFormat: UIGraphicsImageRendererFormat = UIGraphicsImageRendererFormat() | 
					
						
							|  |  |  |         renderFormat.scale = UIScreen.main.scale | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer( | 
					
						
							|  |  |  |             size: stackView.bounds.size, | 
					
						
							|  |  |  |             format: renderFormat | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         return renderer.image { rendererContext in | 
					
						
							|  |  |  |             stackView.layer.render(in: rendererContext.cgContext) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private static func firstSubviewOfType<T>(in superview: UIView) -> T? { | 
					
						
							|  |  |  |         guard !(superview is T) else { return superview as? T } | 
					
						
							|  |  |  |         guard !superview.subviews.isEmpty else { return nil } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         for subview in superview.subviews { | 
					
						
							|  |  |  |             if let result: T = firstSubviewOfType(in: subview) { | 
					
						
							|  |  |  |                 return result | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return nil | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     static func willBeginEditing(indexPath: IndexPath, tableView: UITableView) { | 
					
						
							|  |  |  |         guard | 
					
						
							|  |  |  |             let targetCell: UITableViewCell = tableView.cellForRow(at: indexPath), | 
					
						
							|  |  |  |             targetCell.superview != tableView, | 
					
						
							|  |  |  |             let targetSuperview: UIView = targetCell.superview? | 
					
						
							|  |  |  |                 .subviews | 
					
						
							|  |  |  |                 .filter({ $0 != targetCell }) | 
					
						
							|  |  |  |                 .first, | 
					
						
							|  |  |  |             let side: Side = Side(for: targetSuperview), | 
					
						
							|  |  |  |             let themeMap: [Int: ThemeValue] = UIContextualAction.lookupMap.wrappedValue | 
					
						
							|  |  |  |                 .getting(tableView.hashValue)? | 
					
						
							|  |  |  |                 .getting(side.key(for: indexPath)), | 
					
						
							|  |  |  |             targetSuperview.subviews.count == themeMap.count | 
					
						
							|  |  |  |         else { return } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let targetViews: [UIImageView] = targetSuperview.subviews | 
					
						
							|  |  |  |             .compactMap { subview in firstSubviewOfType(in: subview) } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         guard targetViews.count == themeMap.count else { return } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Set the imageView and background colours (so they change correctly when the theme changes) | 
					
						
							|  |  |  |         targetViews.enumerated().forEach { index, targetView in | 
					
						
							|  |  |  |             guard let themeTintColor: ThemeValue = themeMap[index] else { return } | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             targetView.themeTintColor = themeTintColor | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     static func didEndEditing(indexPath: IndexPath?, tableView: UITableView) { | 
					
						
							|  |  |  |         guard let indexPath: IndexPath = indexPath else { return } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let leadingKey: String = Side.leading.key(for: indexPath) | 
					
						
							|  |  |  |         let trailingKey: String = Side.trailing.key(for: indexPath) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         guard | 
					
						
							|  |  |  |             UIContextualAction.lookupMap.wrappedValue[tableView.hashValue]?[leadingKey] != nil || | 
					
						
							|  |  |  |             UIContextualAction.lookupMap.wrappedValue[tableView.hashValue]?[trailingKey] != nil | 
					
						
							|  |  |  |         else { return } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         UIContextualAction.lookupMap.mutate { | 
					
						
							|  |  |  |             $0[tableView.hashValue]?[leadingKey] = nil | 
					
						
							|  |  |  |             $0[tableView.hashValue]?[trailingKey] = nil | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             if $0[tableView.hashValue]?.isEmpty == true { | 
					
						
							|  |  |  |                 $0[tableView.hashValue] = nil | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |