From 9112231f66e8285f878ebc23d41df91762edf540 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 27 Feb 2023 11:37:25 +1100 Subject: [PATCH] WIP --- Session.xcodeproj/project.pbxproj | 8 +- Session/Home/HomeVC.swift | 23 ++- .../Shared/UIContextualAction+Session.swift | 42 ---- .../UIContextualAction+Theming.swift | 181 ++++++++++++++++++ .../General/Dictionary+Utilities.swift | 6 + 5 files changed, 209 insertions(+), 51 deletions(-) delete mode 100644 Session/Shared/UIContextualAction+Session.swift create mode 100644 SessionUIKit/Utilities/UIContextualAction+Theming.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 561e309c9..358eaf7e4 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -109,11 +109,11 @@ 7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; }; 7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; }; 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; }; + 7B2E985829AC227C001792D7 /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */; }; 7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; }; 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; }; 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; }; - 7B521E0629A87CEA00C3C36A /* UIContextualAction+Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B521E0529A87CEA00C3C36A /* UIContextualAction+Session.swift */; }; 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; }; @@ -1178,11 +1178,11 @@ 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = ""; }; 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = ""; }; 7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; + 7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = ""; }; 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = ""; }; 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = ""; }; 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; 7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = ""; }; - 7B521E0529A87CEA00C3C36A /* UIContextualAction+Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Session.swift"; sourceTree = ""; }; 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = ""; }; @@ -2584,7 +2584,6 @@ FD52090828B59411006098F6 /* ScreenLockUI.swift */, FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */, FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */, - 7B521E0529A87CEA00C3C36A /* UIContextualAction+Session.swift */, ); path = Shared; sourceTree = ""; @@ -2819,6 +2818,7 @@ B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */, C33100272559000A00070591 /* UIView+Utilities.swift */, FD71161F28D97ABC00B47552 /* UIImage+Tinting.swift */, + 7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */, ); path = Utilities; sourceTree = ""; @@ -5142,6 +5142,7 @@ FD37E9D528A1FCE8003AE748 /* Theme+OceanLight.swift in Sources */, FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */, FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */, + 7B2E985829AC227C001792D7 /* UIContextualAction+Theming.swift in Sources */, FD37E9CF28A1EB1B003AE748 /* Theme.swift in Sources */, C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */, FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */, @@ -5715,7 +5716,6 @@ 4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */, B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */, 45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */, - 7B521E0629A87CEA00C3C36A /* UIContextualAction+Session.swift in Sources */, 7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */, 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */, C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */, diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 3ed8a59ca..522a54db9 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -626,7 +626,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi case .messageRequests: return nil case .threads: - + let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] return UISwipeActionsConfiguration(actions: [ ]) default: return nil } @@ -649,8 +649,15 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi case .threads: let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] let delete: UIContextualAction = UIContextualAction( - style: .destructive, - title: "TXT_DELETE_TITLE".localized() + title: "TXT_DELETE_TITLE".localized(), + icon: UIImage(named: "icon_bin"), + iconHeight: 5, + themeTintColor: .textPrimary, + themeBackgroundColor: .conversationButton_swipeDestructive, + side: .trailing, + actionIndex: 2, + indexPath: indexPath, + tableView: tableView ) { [weak self] _, _, completionHandler in let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( @@ -679,7 +686,6 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi self?.present(confirmationModal, animated: true, completion: nil) } delete.themeBackgroundColor = .conversationButton_swipeDestructive - delete.setupSessionStyle(with: UIImage(systemName: "trash")) let pin: UIContextualAction = UIContextualAction( style: .normal, @@ -703,7 +709,6 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi } } pin.themeBackgroundColor = .conversationButton_swipeTertiary - pin.setupSessionStyle(with: UIImage(systemName: "pin")) guard threadViewModel.threadVariant == .contact && !threadViewModel.threadIsNoteToSelf else { return UISwipeActionsConfiguration(actions: [ delete, pin ]) @@ -749,6 +754,14 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi } } + func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) { + UIContextualAction.willBeginEditing(indexPath: indexPath, tableView: tableView) + } + + func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { + UIContextualAction.didEndEditing(indexPath: indexPath, tableView: tableView) + } + // MARK: - Interaction func handleContinueButtonTapped(from seedReminderView: SeedReminderView) { diff --git a/Session/Shared/UIContextualAction+Session.swift b/Session/Shared/UIContextualAction+Session.swift deleted file mode 100644 index 64e01217c..000000000 --- a/Session/Shared/UIContextualAction+Session.swift +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import UIKit - -extension UIContextualAction { - - func setupSessionStyle(with image: UIImage?) { - guard let title = self.title, let image = image else { - self.image = image - return - } - - let text = NSMutableAttributedString(string: "") - let attachment = NSTextAttachment() - attachment.image = image.withTintColor(.white) - text.append(NSAttributedString(attachment: attachment)) - text.append( - NSAttributedString( - string: "\n\(title)", - attributes: [ - .font : UIFont.systemFont(ofSize: Values.smallFontSize), - .foregroundColor : UIColor.white - ] - ) - ) - - let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) - label.textAlignment = .center - label.numberOfLines = 2 - label.attributedText = text - - let renderer = UIGraphicsImageRenderer(bounds: label.bounds) - let renderedImage = renderer.image { context in - label.layer.render(in: context.cgContext) - } - if let cgImage = renderedImage.cgImage { - let finalImage = UIImage(cgImage: cgImage, scale: UIScreen.main.scale, orientation: .up) - self.image = finalImage - self.title = nil - } - } -} diff --git a/SessionUIKit/Utilities/UIContextualAction+Theming.swift b/SessionUIKit/Utilities/UIContextualAction+Theming.swift new file mode 100644 index 000000000..99e563e97 --- /dev/null +++ b/SessionUIKit/Utilities/UIContextualAction+Theming.swift @@ -0,0 +1,181 @@ +// 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 = .textPrimary, + 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 = 3 + + 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.verySmallFontSize) + 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(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 + } + } + } +} diff --git a/SessionUtilitiesKit/General/Dictionary+Utilities.swift b/SessionUtilitiesKit/General/Dictionary+Utilities.swift index d05bfe761..694844492 100644 --- a/SessionUtilitiesKit/General/Dictionary+Utilities.swift +++ b/SessionUtilitiesKit/General/Dictionary+Utilities.swift @@ -34,6 +34,12 @@ public extension Dictionary { return self[key] } + func getting(_ key: Key?) -> Value? { + guard let key: Key = key else { return nil } + + return self[key] + } + func setting(_ key: Key?, _ value: Value?) -> [Key: Value] { guard let key: Key = key else { return self }