From 68a831116643e5e4649c848a05911c31d5b4eb6b Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 7 Feb 2024 12:09:54 +1100 Subject: [PATCH] add auto deletes subtitle on delete action in long press menu screen --- .../Context Menu/ContextMenuVC+Action.swift | 19 +++++ .../ContextMenuVC+ActionView.swift | 64 +++++++++++++-- .../General/String+Utilities.swift | 80 ++++++++++++++++++- .../General/TimeInterval+Utilities.swift | 1 + 4 files changed, 157 insertions(+), 7 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 327a76c49..7e3e9f6e4 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -3,11 +3,19 @@ import UIKit import SessionMessagingKit import SessionUtilitiesKit +import SessionUIKit extension ContextMenuVC { + struct ExpirationInfo { + let expiresStartedAtMs: Double? + let expiresInSeconds: TimeInterval? + } + struct Action { let icon: UIImage? let title: String + let expirationInfo: ExpirationInfo? + let themeColor: ThemeValue let isEmojiAction: Bool let isEmojiPlus: Bool let isDismissAction: Bool @@ -19,6 +27,8 @@ extension ContextMenuVC { init( icon: UIImage? = nil, title: String = "", + expirationInfo: ExpirationInfo? = nil, + themeColor: ThemeValue = .textPrimary, isEmojiAction: Bool = false, isEmojiPlus: Bool = false, isDismissAction: Bool = false, @@ -27,6 +37,8 @@ extension ContextMenuVC { ) { self.icon = icon self.title = title + self.expirationInfo = expirationInfo + self.themeColor = themeColor self.isEmojiAction = isEmojiAction self.isEmojiPlus = isEmojiPlus self.isDismissAction = isDismissAction @@ -84,6 +96,11 @@ extension ContextMenuVC { return Action( icon: UIImage(named: "ic_trash"), title: "TXT_DELETE_TITLE".localized(), + expirationInfo: ExpirationInfo( + expiresStartedAtMs: cellViewModel.expiresStartedAtMs, + expiresInSeconds: cellViewModel.expiresInSeconds + ), + themeColor: .danger, accessibilityLabel: "Delete message" ) { delegate?.delete(cellViewModel, using: dependencies) } } @@ -100,6 +117,7 @@ extension ContextMenuVC { return Action( icon: UIImage(named: "ic_block"), title: "context_menu_ban_user".localized(), + themeColor: .danger, accessibilityLabel: "Ban user" ) { delegate?.ban(cellViewModel, using: dependencies) } } @@ -108,6 +126,7 @@ extension ContextMenuVC { return Action( icon: UIImage(named: "ic_block"), title: "context_menu_ban_and_delete_all".localized(), + themeColor: .danger, accessibilityLabel: "Ban user and delete" ) { delegate?.banAndDeleteAllMessages(cellViewModel, using: dependencies) } } diff --git a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift index ac061dd5d..3701cae96 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift @@ -3,6 +3,7 @@ import UIKit import SessionUIKit import SessionUtilitiesKit +import SessionSnodeKit extension ContextMenuVC { final class ActionView: UIView { @@ -12,23 +13,32 @@ extension ContextMenuVC { private let action: Action private let dismiss: () -> Void private var didTouchDownInside: Bool = false + private var timer: Timer? // MARK: - UI - private let iconImageView: UIImageView = { + private lazy var iconImageView: UIImageView = { let result: UIImageView = UIImageView() result.contentMode = .center - result.themeTintColor = .textPrimary + result.themeTintColor = action.themeColor result.set(.width, to: ActionView.iconImageViewSize) result.set(.height, to: ActionView.iconImageViewSize) return result }() - private let titleLabel: UILabel = { + private lazy var titleLabel: UILabel = { let result: UILabel = UILabel() result.font = .systemFont(ofSize: Values.mediumFontSize) - result.themeTextColor = .textPrimary + result.themeTextColor = action.themeColor + + return result + }() + + private lazy var subtitleLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.miniFontSize) + result.themeTextColor = action.themeColor return result }() @@ -59,9 +69,18 @@ extension ContextMenuVC { .resizedImage(to: CGSize(width: ActionView.iconSize, height: ActionView.iconSize))? .withRenderingMode(.alwaysTemplate) titleLabel.text = action.title + setUpSubtitle() + + let labelContainer: UIView = UIView() + labelContainer.set(.width, greaterThanOrEqualTo: 115) + labelContainer.addSubview(titleLabel) + labelContainer.addSubview(subtitleLabel) + titleLabel.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: labelContainer) + subtitleLabel.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: labelContainer) + titleLabel.pin(.bottom, to: .top, of: subtitleLabel) // Stack view - let stackView: UIStackView = UIStackView(arrangedSubviews: [ iconImageView, titleLabel ]) + let stackView: UIStackView = UIStackView(arrangedSubviews: [ iconImageView, labelContainer ]) stackView.axis = .horizontal stackView.spacing = Values.smallSpacing stackView.alignment = .center @@ -82,11 +101,44 @@ extension ContextMenuVC { addGestureRecognizer(tapGestureRecognizer) } + private func setUpSubtitle() { + guard + let expiresInSeconds = self.action.expirationInfo?.expiresInSeconds, + let expiresStartedAtMs = self.action.expirationInfo?.expiresStartedAtMs + else { + subtitleLabel.isHidden = true + return + } + + subtitleLabel.isHidden = false + let timeToExpireInSeconds: TimeInterval = (expiresStartedAtMs + expiresInSeconds * 1000 - Double(SnodeAPI.currentOffsetTimestampMs())) / 1000 + subtitleLabel.text = "Auto-deletes in \(timeToExpireInSeconds.formatted(format: .twoUnits))" + + timer = Timer.scheduledTimerOnMainThread(withTimeInterval: 1, repeats: true, block: { [weak self] _ in + let timeToExpireInSeconds: TimeInterval = (expiresStartedAtMs + expiresInSeconds * 1000 - Double(SnodeAPI.currentOffsetTimestampMs())) / 1000 + if timeToExpireInSeconds <= 0 { + self?.dismissWithTimerInvalidationIfNeeded() + } else { + self?.subtitleLabel.text = "Auto-deletes in \(timeToExpireInSeconds.formatted(format: .twoUnits))" + } + }) + } + + override func removeFromSuperview() { + self.timer?.invalidate() + super.removeFromSuperview() + } + // MARK: - Interaction + private func dismissWithTimerInvalidationIfNeeded() { + self.timer?.invalidate() + dismiss() + } + @objc private func handleTap() { action.work() - dismiss() + dismissWithTimerInvalidationIfNeeded() } override func touchesBegan(_ touches: Set, with event: UIEvent?) { diff --git a/SessionUtilitiesKit/General/String+Utilities.swift b/SessionUtilitiesKit/General/String+Utilities.swift index 42cd4c5a9..dc41145eb 100644 --- a/SessionUtilitiesKit/General/String+Utilities.swift +++ b/SessionUtilitiesKit/General/String+Utilities.swift @@ -251,6 +251,84 @@ public extension String { ) ) } - } + case .twoUnits: + let seconds: Int = Int(duration.truncatingRemainder(dividingBy: 60)) + let minutes: Int = Int((duration / 60).truncatingRemainder(dividingBy: 60)) + let hours: Int = Int((duration / 3600).truncatingRemainder(dividingBy: 24)) + let days: Int = Int((duration / 3600 / 24).truncatingRemainder(dividingBy: 7)) + let weeks: Int = Int(duration / 3600 / 24 / 7) + + guard weeks == 0 else { + return String( + format: "TIME_AMOUNT_WEEKS_SHORT_FORMAT".localized(), + NumberFormatter.localizedString( + from: NSNumber(integerLiteral: weeks), + number: .none + ) + ) + " " + String( + format: "TIME_AMOUNT_DAYS_SHORT_FORMAT".localized(), + NumberFormatter.localizedString( + from: NSNumber(integerLiteral: days), + number: .none + ) + ) + } + + guard days == 0 else { + return String( + format: "TIME_AMOUNT_DAYS_SHORT_FORMAT".localized(), + NumberFormatter.localizedString( + from: NSNumber(integerLiteral: days), + number: .none + ) + ) + " " + String( + format: "TIME_AMOUNT_HOURS_SHORT_FORMAT".localized(), + NumberFormatter.localizedString( + from: NSNumber(integerLiteral: hours), + number: .none + ) + ) + } + + guard hours == 0 else { + return String( + format: "TIME_AMOUNT_HOURS_SHORT_FORMAT".localized(), + NumberFormatter.localizedString( + from: NSNumber(integerLiteral: hours), + number: .none + ) + ) + " " + String( + format: "TIME_AMOUNT_MINUTES_SHORT_FORMAT".localized(), + NumberFormatter.localizedString( + from: NSNumber(integerLiteral: minutes), + number: .none + ) + ) + } + + guard minutes == 0 else { + return String( + format: "TIME_AMOUNT_MINUTES_SHORT_FORMAT".localized(), + NumberFormatter.localizedString( + from: NSNumber(integerLiteral: minutes), + number: .none + ) + ) + " " + String( + format: "TIME_AMOUNT_SECONDS_SHORT_FORMAT".localized(), + NumberFormatter.localizedString( + from: NSNumber(integerLiteral: seconds), + number: .none + ) + ) + } + + return String( + format: "TIME_AMOUNT_SECONDS_SHORT_FORMAT".localized(), + NumberFormatter.localizedString( + from: NSNumber(integerLiteral: seconds), + number: .none + ) + ) + } } } diff --git a/SessionUtilitiesKit/General/TimeInterval+Utilities.swift b/SessionUtilitiesKit/General/TimeInterval+Utilities.swift index c0c668374..1be8ee0ac 100644 --- a/SessionUtilitiesKit/General/TimeInterval+Utilities.swift +++ b/SessionUtilitiesKit/General/TimeInterval+Utilities.swift @@ -8,6 +8,7 @@ public extension TimeInterval { case long case hoursMinutesSeconds case videoDuration + case twoUnits } func formatted(format: DurationFormat) -> String {