mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/5b7576a8-e0c0-4036-8b7e-8f2e6fbfa4d7 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
123 lines
3.6 KiB
Swift
123 lines
3.6 KiB
Swift
//
|
|
// RateLimitCountdownView.swift
|
|
// Meshtastic
|
|
//
|
|
// Created by Jake Bordens on 5/5/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
// This class provides a rate limited button.
|
|
// Provide a key to differentiate which action is rate-limited
|
|
// This allows you to keep different rate limits for different action
|
|
// Rate limits are stored in a RateLimitStorage singleton, but do not persist
|
|
public struct RateLimitedButton<Content: View>: View {
|
|
typealias Builder = ((percentComplete: Double, secondsRemaining: TimeInterval)?) -> Content
|
|
|
|
let key: String
|
|
|
|
@StateObject var storage = RateLimitStorage.shared
|
|
|
|
let rateLimit: TimeInterval
|
|
let content: Builder
|
|
let action: () -> Void
|
|
|
|
init(key: String, rateLimit: TimeInterval, action: @escaping () -> Void, @ViewBuilder label: @escaping Builder) {
|
|
self.key = key
|
|
self.rateLimit = rateLimit
|
|
self.content = label
|
|
self.action = action
|
|
}
|
|
|
|
public var body: some View {
|
|
let percentRemaining = storage.rateLimitRemainingPercentage(forKey: key)
|
|
let secondsRemaining = storage.rateLimitSecondsRemaining(forKey: key)
|
|
if percentRemaining > 0.0 {
|
|
content((percentRemaining, secondsRemaining))
|
|
} else {
|
|
Button {
|
|
storage.actionOccured(forKey: key, rateLimit: rateLimit)
|
|
action()
|
|
} label: {
|
|
content(nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
RateLimitedButton(key: "preview", rateLimit: 30, action: { }) { rateLimitInfo in
|
|
if let info = rateLimitInfo {
|
|
Label("\(Int(info.secondsRemaining))s", systemImage: "clock")
|
|
} else {
|
|
Label("Send", systemImage: "paperplane")
|
|
}
|
|
}
|
|
}
|
|
|
|
// To store the time an action occured (name by a key) and the time limit
|
|
// Does not persist across app launches
|
|
class RateLimitStorage: ObservableObject {
|
|
private struct RateLimiter {
|
|
var actionOccuredTimestamp: Date
|
|
var rateLimitSeconds: TimeInterval
|
|
|
|
var rateLimitExpires: Date {
|
|
return actionOccuredTimestamp.addingTimeInterval(rateLimitSeconds)
|
|
}
|
|
}
|
|
|
|
static var shared: RateLimitStorage = RateLimitStorage() // Singleton instance
|
|
|
|
private var rateLimits = [String: RateLimiter]()
|
|
private var timer: Timer?
|
|
|
|
func actionOccured(forKey key: String, rateLimit: TimeInterval) {
|
|
let now = Date()
|
|
if let existingRateLimit = rateLimits[key] {
|
|
if existingRateLimit.rateLimitExpires > now.addingTimeInterval(rateLimit) {
|
|
// We have an existing rate limit that is larger than the one being requested
|
|
// Ignore
|
|
return
|
|
}
|
|
}
|
|
self.objectWillChange.send()
|
|
rateLimits[key] = RateLimiter(actionOccuredTimestamp: now, rateLimitSeconds: rateLimit)
|
|
startTimerIfNecessary()
|
|
}
|
|
|
|
func rateLimitRemainingPercentage(forKey: String) -> Double {
|
|
guard let rateLimit = rateLimits[forKey] else {
|
|
return 0.0
|
|
}
|
|
let percent = (rateLimit.rateLimitExpires.timeIntervalSinceNow) / rateLimit.rateLimitSeconds
|
|
return min(1.0, max(percent, 0.0))
|
|
}
|
|
|
|
func rateLimitSecondsRemaining(forKey: String) -> TimeInterval {
|
|
guard let rateLimit = rateLimits[forKey] else {
|
|
return 0.0
|
|
}
|
|
return rateLimit.rateLimitExpires.timeIntervalSinceNow
|
|
}
|
|
|
|
func startTimerIfNecessary() {
|
|
// Timer exists, don't create one
|
|
guard timer == nil else { return }
|
|
|
|
// Create the timer
|
|
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
guard let self = self else { return }
|
|
self.objectWillChange.send()
|
|
|
|
// Determine if we can clean up the dictionary and stop the timer.
|
|
let maxExpiration = self.rateLimits.values.map { $0.rateLimitExpires }.max() ?? .distantPast
|
|
if maxExpiration.timeIntervalSinceNow < 0 {
|
|
// All rateLimits are in the past. Stop and clean up
|
|
self.timer?.invalidate()
|
|
self.timer = nil
|
|
self.rateLimits.removeAll()
|
|
}
|
|
}
|
|
}
|
|
}
|