This commit is contained in:
Ryan Zhao 2023-02-27 11:37:25 +11:00
parent b3378992ed
commit 9112231f66
5 changed files with 209 additions and 51 deletions

View File

@ -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 = "<group>"; };
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = "<group>"; };
7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = "<group>"; };
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = "<group>"; };
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = "<group>"; };
7B521E0529A87CEA00C3C36A /* UIContextualAction+Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Session.swift"; sourceTree = "<group>"; };
7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = "<group>"; };
7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = "<group>"; };
@ -2584,7 +2584,6 @@
FD52090828B59411006098F6 /* ScreenLockUI.swift */,
FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */,
FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */,
7B521E0529A87CEA00C3C36A /* UIContextualAction+Session.swift */,
);
path = Shared;
sourceTree = "<group>";
@ -2819,6 +2818,7 @@
B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */,
C33100272559000A00070591 /* UIView+Utilities.swift */,
FD71161F28D97ABC00B47552 /* UIImage+Tinting.swift */,
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -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 */,

View File

@ -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) {

View File

@ -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
}
}
}

View File

@ -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<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
}
}
}
}

View File

@ -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 }