Skip to content

Commit

Permalink
[LOOP-4349] Temporary mute alerts (#526)
Browse files Browse the repository at this point in the history
* added placeholder UI to configure mute alerts duration

* checkpoint

* checkpoint

* initial commit at the alert muter

* minor clean-up

* removed unused file from project

* corrected predicate syntax

* all alerts at least vibrate

* checkpoint

* insteadOf = https://github.com/
updating unit tests

* checkpoint

* tighter coupling between alert manager and in-app/user notification alert issuers

* corrected rescheduling loop not running notifications and use timers for UI

* make the mute end period timer internval to the alert muter

* corrected typo

* updated unit tests

* added duration to temp mute alerts setting

* minor clean-up

* using source of truth for last loop date

* renamed Issuer -> Scheduler for in-app and user notification
  • Loading branch information
nhamming authored Oct 28, 2022
1 parent a44e5e7 commit 635ade6
Show file tree
Hide file tree
Showing 23 changed files with 957 additions and 352 deletions.
46 changes: 28 additions & 18 deletions Loop.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Loop/Extensions/SettingsStore+SimulatedCoreData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ fileprivate extension StoredSettings {
providesAppNotificationSettings: true,
announcementSetting: .enabled,
timeSensitiveSetting: .enabled,
scheduledDeliverySetting: .disabled)
scheduledDeliverySetting: .disabled,
temporaryMuteAlertsSetting: .disabled)
let controllerDevice = StoredSettings.ControllerDevice(name: "Controller Name",
systemName: "Controller System Name",
systemVersion: "Controller System Version",
Expand Down
128 changes: 128 additions & 0 deletions Loop/Managers/AlertMuter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// AlertMuter.swift
// Loop
//
// Created by Nathaniel Hamming on 2022-09-14.
// Copyright © 2022 LoopKit Authors. All rights reserved.
//

import Foundation
import Combine
import SwiftUI
import LoopKit

public class AlertMuter: ObservableObject {
struct Configuration: Equatable, RawRepresentable {
typealias RawValue = [String: Any]

enum ConfigurationKey: String {
case duration
case startTime
}

init?(rawValue: [String : Any]) {
guard let duration = rawValue[ConfigurationKey.duration.rawValue] as? TimeInterval
else { return nil }

self.duration = duration
self.startTime = rawValue[ConfigurationKey.startTime.rawValue] as? Date
}

var rawValue: [String : Any] {
var rawValue: [String : Any] = [:]
rawValue[ConfigurationKey.duration.rawValue] = duration
rawValue[ConfigurationKey.startTime.rawValue] = startTime
return rawValue
}

var duration: TimeInterval

var startTime: Date?

var shouldMute: Bool {
guard let mutingEndTime = mutingEndTime else { return false }
return mutingEndTime >= Date()
}

var mutingEndTime: Date? {
startTime?.addingTimeInterval(duration)
}

init(startTime: Date? = nil, duration: TimeInterval = AlertMuter.allowedDurations[0]) {
self.duration = duration
self.startTime = startTime
}

func shouldMuteAlert(scheduledAt timeFromNow: TimeInterval = 0, now: Date = Date()) -> Bool {
guard timeFromNow >= 0 else { return false }

guard let mutingEndTime = mutingEndTime else { return false }

let alertTriggerTime = now.advanced(by: timeFromNow)
guard alertTriggerTime < mutingEndTime
else { return false }

return true
}
}

@Published var configuration: Configuration {
didSet {
if oldValue != configuration {
updateMutePeriodEndingWatcher()
}
}
}

private var mutePeriodEndingTimer: Timer?

private lazy var cancellables = Set<AnyCancellable>()

static var allowedDurations: [TimeInterval] { [.minutes(30), .hours(1), .hours(2), .hours(4)] }

init(configuration: Configuration = Configuration()) {
self.configuration = configuration

NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
.sink { [weak self] _ in
self?.updateMutePeriodEndingWatcher()
}
.store(in: &cancellables)

updateMutePeriodEndingWatcher()
}

convenience init(startTime: Date? = nil, duration: TimeInterval = AlertMuter.allowedDurations[0]) {
self.init(configuration: Configuration(startTime: startTime, duration: duration))
}

private func updateMutePeriodEndingWatcher(_ now: Date = Date()) {
mutePeriodEndingTimer?.invalidate()

guard let mutingEndTime = configuration.mutingEndTime else { return }

guard mutingEndTime > now else {
configuration.startTime = nil
return
}

let timeInterval = mutingEndTime.timeIntervalSince(now)
mutePeriodEndingTimer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] _ in
self?.configuration.startTime = nil
}
}

func shouldMuteAlert(scheduledAt timeFromNow: TimeInterval = 0) -> Bool {
return configuration.shouldMuteAlert(scheduledAt: timeFromNow)
}

func shouldMuteAlert(_ alert: LoopKit.Alert, issuedDate: Date? = nil, now: Date = Date()) -> Bool {
switch alert.trigger {
case .immediate:
return shouldMuteAlert(scheduledAt: (issuedDate ?? now).timeIntervalSince(now))
case .delayed(let interval), .repeating(let interval):
let triggerInterval = ((issuedDate ?? now) + interval).timeIntervalSince(now)
return shouldMuteAlert(scheduledAt: triggerInterval)
}
}
}
Loading

0 comments on commit 635ade6

Please sign in to comment.