TM-SGNL-iOS/Signal/ConversationView/CellViews/MessageTimerView.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

157 lines
5.3 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
final class MessageTimerView: ManualLayoutView {
private enum Constants {
static let layoutSize: CGFloat = 12
// 0 == about to expire, 12 == just started countdown.
static let quantizationLevelCount: UInt64 = 12
}
private let imageView = CVImageView()
private var animationTimer: Timer?
init() {
super.init(name: "OWSMessageTimerView")
addSubviewToFillSuperviewEdges(imageView)
}
deinit {
clearAnimation()
}
func configure(
expirationTimestampMs: UInt64,
disappearingMessageInterval: UInt32,
tintColor: UIColor
) {
let expirationProgress = self.expirationProgress(
expirationTimestampMs: expirationTimestampMs,
disappearingMessageInterval: disappearingMessageInterval,
nowMs: Date.ows_millisecondTimestamp()
)
updateIcon(quantizedValue: expirationProgress.quantizedValue, tintColor: tintColor)
startAnimation(expirationProgress: expirationProgress, tintColor: tintColor)
}
private struct ExpirationProgress {
var quantizedValue: UInt64
var timerConfiguration: TimerConfiguration?
}
private struct TimerConfiguration {
let nextRefreshMs: UInt64
let refreshIntervalMs: UInt64
}
private func expirationProgress(
expirationTimestampMs: UInt64,
disappearingMessageInterval: UInt32,
nowMs: UInt64
) -> ExpirationProgress {
// Every N milliseconds we move to the next progress level.
let refreshIntervalMs = UInt64(disappearingMessageInterval) * 1000 / Constants.quantizationLevelCount
// It will never expire because the timer hasn't started yet.
guard expirationTimestampMs > 0, refreshIntervalMs > 0 else {
return ExpirationProgress(quantizedValue: Constants.quantizationLevelCount)
}
let remainingMs = expirationTimestampMs.subtractingReportingOverflow(nowMs)
// It already expired because the expiration date is in the past.
guard !remainingMs.overflow else {
return ExpirationProgress(quantizedValue: 0)
}
let intermediateValue = remainingMs.partialValue.addingReportingOverflow(refreshIntervalMs/2)
// The disappearing interval is way too large -- something is wrong.
guard !intermediateValue.overflow else {
return ExpirationProgress(quantizedValue: Constants.quantizationLevelCount)
}
let quantizedValue = intermediateValue.partialValue / refreshIntervalMs
return ExpirationProgress(
quantizedValue: quantizedValue,
timerConfiguration: {
guard quantizedValue > 0 else {
return nil
}
return TimerConfiguration(
nextRefreshMs: expirationTimestampMs - (quantizedValue - 1) * refreshIntervalMs - refreshIntervalMs/2,
refreshIntervalMs: refreshIntervalMs
)
}()
)
}
private func updateIcon(quantizedValue: UInt64, tintColor: UIColor) {
let progressIcon = self.progressIcon(quantizedValue: quantizedValue, tintColor: tintColor)
imageView.image = progressIcon?.withRenderingMode(.alwaysTemplate)
imageView.tintColor = tintColor
}
private func progressIcon(quantizedValue: UInt64, tintColor: UIColor) -> UIImage? {
owsAssertDebug(quantizedValue <= Constants.quantizationLevelCount)
let imageName = String(format: "messagetimer-%02ld", quantizedValue * 5)
guard let image = UIImage(named: imageName) else {
owsFailDebug("Missing icon.")
return nil
}
owsAssertDebug(image.size.width == Constants.layoutSize)
owsAssertDebug(image.size.height == Constants.layoutSize)
return image
}
private func startAnimation(expirationProgress: ExpirationProgress, tintColor: UIColor) {
AssertIsOnMainThread()
clearAnimation()
guard let timerConfiguration = expirationProgress.timerConfiguration else {
return
}
var quantizedValue = expirationProgress.quantizedValue
let animationTimer = Timer(
fire: Date(millisecondsSince1970: timerConfiguration.nextRefreshMs),
interval: TimeInterval(timerConfiguration.refreshIntervalMs)/1000,
repeats: true,
block: { [weak self] timer in
guard let self else {
timer.invalidate()
return
}
quantizedValue -= 1
self.updateIcon(quantizedValue: quantizedValue, tintColor: tintColor)
guard quantizedValue > 0 else {
timer.invalidate()
return
}
}
)
self.animationTimer = animationTimer
RunLoop.main.add(animationTimer, forMode: .common)
}
private func clearAnimation() {
AssertIsOnMainThread()
animationTimer?.invalidate()
animationTimer = nil
}
func prepareForReuse() {
clearAnimation()
imageView.image = nil
}
static var measureSize: CGSize {
.square(Constants.layoutSize)
}
}