فهرست منبع

Alerts: PR review fixes (alertId enum prop, podexpiring → reminder, drop banner swipe)

Deniz Cengiz 3 هفته پیش
والد
کامیت
dc8060c315

+ 9 - 34
Trio/Sources/APS/APSManager.swift

@@ -1287,25 +1287,8 @@ final class BaseAPSManager: APSManager, Injectable {
             body: body,
             acknowledgeActionButtonLabel: String(localized: "OK")
         )
-        let alertId: String
-        switch category {
-        case .bolusFailed: alertId = "bolusFailed"
-        case .hardwareFault: alertId = "hardwareFault"
-        case .deliveryUncertain: alertId = "deliveryUncertain"
-        case .occlusion: alertId = "occlusion"
-        case .reservoirEmpty: alertId = "reservoirEmpty"
-        case .reservoirLow: alertId = "reservoirLow"
-        case .batteryEmpty: alertId = "batteryEmpty"
-        case .batteryLow: alertId = "batteryLow"
-        case .manualTempBasalActive: alertId = "manualTempBasalActive"
-        case .sensorFailure: alertId = "sensorFailure"
-        case .deviceExpired: alertId = "deviceExpired"
-        case .deviceExpirationReminder: alertId = "deviceExpirationReminder"
-        case .glucoseDataStale: alertId = "glucoseDataStale"
-        default: alertId = "general"
-        }
         let alert = Alert(
-            identifier: Alert.Identifier(managerIdentifier: "trio.aps", alertIdentifier: alertId),
+            identifier: Alert.Identifier(managerIdentifier: "trio.aps", alertIdentifier: category.alertIdentifier),
             foregroundContent: content,
             backgroundContent: content,
             trigger: .immediate,
@@ -1315,14 +1298,14 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func issueAlertForError(_ error: Error, category: TrioAlertCategory) {
-        let (alertId, title, body) = describeForAlert(error, category: category)
+        let (title, body) = describeForAlert(error)
         let content = Alert.Content(
             title: title,
             body: body,
             acknowledgeActionButtonLabel: "OK"
         )
         let alert = Alert(
-            identifier: Alert.Identifier(managerIdentifier: "trio.aps", alertIdentifier: alertId),
+            identifier: Alert.Identifier(managerIdentifier: "trio.aps", alertIdentifier: category.alertIdentifier),
             foregroundContent: content,
             backgroundContent: content,
             trigger: .immediate,
@@ -1331,30 +1314,22 @@ final class BaseAPSManager: APSManager, Injectable {
         trioAlertManager?.issueAlert(alert)
     }
 
-    private func describeForAlert(
-        _ error: Error,
-        category _: TrioAlertCategory
-    ) -> (alertId: String, title: String, body: String) {
+    private func describeForAlert(_ error: Error) -> (title: String, body: String) {
         if let apsError = error as? APSError {
             switch apsError {
             case let .pumpError(inner):
                 return (
-                    "pumpError",
                     String(localized: "Pump Error"),
                     String(localized: "Trio could not communicate with the pump. Check the pump and try again.")
                         + "\n\n\(inner.localizedDescription)"
                 )
-            case let .invalidPumpState(message):
-                return ("invalidPumpState", String(localized: "Pump State"), message)
-            case let .glucoseError(message):
-                return ("glucoseError", String(localized: "Glucose"), message)
-            case let .apsError(message):
-                return ("algorithmError", String(localized: "Algorithm"), message)
-            case let .manualBasalTemp(message):
-                return ("manualBasalTemp", String(localized: "Manual Temp Basal"), message)
+            case let .invalidPumpState(message): return (String(localized: "Pump State"), message)
+            case let .glucoseError(message): return (String(localized: "Glucose"), message)
+            case let .apsError(message): return (String(localized: "Algorithm"), message)
+            case let .manualBasalTemp(message): return (String(localized: "Manual Temp Basal"), message)
             }
         }
-        return ("general", "Trio", error.localizedDescription)
+        return ("Trio", error.localizedDescription)
     }
 
     private func createBolusReporter() {

+ 1 - 1
Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmAudioSection.swift

@@ -141,7 +141,7 @@ private struct TonePickerRow: View {
             player = p
             playing = filename
         } catch {
-            // best-effort preview — ignore
+            debug(.service, "Audio preview failed for \(filename): \(error)")
         }
     }
 

+ 2 - 2
Trio/Sources/Services/Alerts/GlucoseAlertCoordinator.swift

@@ -248,8 +248,8 @@ final class GlucoseAlertCoordinator: Injectable {
             )
         case .forecastedLow:
             return String(
-                format: String(localized: "Forecast +20 min %1$@, at or below limit %2$@."),
-                valueString, limitString
+                format: String(localized: "Forecast +%1$d min %2$@, at or below limit %3$@."),
+                ForecastedGlucoseEvaluator.defaultHorizonMinutes, valueString, limitString
             )
         case .high:
             return String(

+ 44 - 7
Trio/Sources/Services/Alerts/TrioAlertCategory.swift

@@ -82,6 +82,37 @@ enum TrioAlertCategory: Equatable {
         }
     }
 
+    /// Canonical slug used when constructing `Alert.Identifier`. Round-trips
+    /// through `TrioAlertClassifier.categorize(alertIdentifier:)` — keep the
+    /// substring matchers in the classifier in sync with these slugs.
+    var alertIdentifier: String {
+        switch self {
+        case .occlusion: return "occlusion"
+        case .reservoirLow: return "reservoirLow"
+        case .reservoirEmpty: return "reservoirEmpty"
+        case .batteryLow: return "batteryLow"
+        case .batteryEmpty: return "batteryEmpty"
+        case .hardwareFault: return "hardwareFault"
+        case .deliveryUncertain: return "deliveryUncertain"
+        case .deviceExpirationReminder: return "deviceExpirationReminder"
+        case .deviceExpired: return "deviceExpired"
+        case .podShutdownImminent: return "podShutdownImminent"
+        case .suspendTimeExpired: return "suspendTimeExpired"
+        case .bolusFailed: return "bolusFailed"
+        case .manualTempBasalActive: return "manualTempBasalActive"
+        case .notLooping: return "notLooping"
+        case .sensorFailure: return "sensorFailure"
+        case .glucoseUrgentLow: return "glucoseUrgentLow"
+        case .glucoseLow: return "glucoseLow"
+        case .glucoseForecastedLow: return "glucoseForecastedLow"
+        case .glucoseHigh: return "glucoseHigh"
+        case .glucoseDataStale: return "glucoseDataStale"
+        case .algorithmError: return "algorithmError"
+        case .commsTransient: return "commsTransient"
+        case let .other(id): return id
+        }
+    }
+
     var interruptionLevel: Alert.InterruptionLevel {
         switch self {
         case .batteryEmpty,
@@ -178,20 +209,26 @@ enum TrioAlertClassifier {
             return .sensorFailure
         }
 
+        // Expiration reminders — F3, pre-expiry warnings (pod expiring, sensor
+        // grace period, calibration grace). Checked BEFORE `.deviceExpired`
+        // so "podexpiring" / "sensorgrace" don't get swallowed by the
+        // generic `expired` substring catch-all below.
+        if id.contains("podexpiring") || id.contains("expirationreminder") || id.contains("userpodexpiration")
+            || id.contains("retiringsoon") || id.contains("sensorending") || id.contains("calibrationgrace")
+            || id.contains("sensorgrace") || id.contains("graceperiod")
+        {
+            return .deviceExpirationReminder
+        }
+
         // Device expired — N6 (pod, sensor, transmitter end-of-life).
-        if id.contains("podexpired") || id.contains("podexpiring") || id.contains("sensorexpired")
+        if id.contains("podexpired") || id.contains("sensorexpired")
             || id.contains("sensorretired") || id.contains("transmittereol") || id.contains("sensoragedout")
-            || id.contains("mspalarm") || id.contains("expiredsensor") || id.contains("sensorgrace")
+            || id.contains("mspalarm") || id.contains("expiredsensor")
             || (id.contains("expired") && !id.contains("suspendtimeexpired"))
         {
             return .deviceExpired
         }
         if id.contains("shutdownimminent") || id.contains("expireimminent") { return .podShutdownImminent }
-        if id.contains("expirationreminder") || id.contains("userpodexpiration") || id.contains("retiringsoon")
-            || id.contains("sensorending") || id.contains("calibrationgrace") || id.contains("gracePeriod".lowercased())
-        {
-            return .deviceExpirationReminder
-        }
 
         // Low-supply warnings — F1 / F2.
         if id.contains("lowreservoir") || id.contains("reservoirlow") || id.contains("remaininginsulin") {

+ 6 - 32
Trio/Sources/Services/Alerts/TrioModalAlertScheduler.swift

@@ -123,14 +123,16 @@ final class TrioModalAlertScheduler: ObservableObject {
     }
 
     private func sortByPriority() {
-        active.sort { Self.priority(of: $0.interruptionLevel) > Self.priority(of: $1.interruptionLevel) }
+        active.sort { Self.rank(of: $0.interruptionLevel) < Self.rank(of: $1.interruptionLevel) }
     }
 
-    private static func priority(of level: LoopKit.Alert.InterruptionLevel) -> Int {
+    /// Lower rank = higher severity. Matches `DeviceAlertsStore.severityRank`
+    /// so the sort convention is consistent across the alert pipeline.
+    private static func rank(of level: LoopKit.Alert.InterruptionLevel) -> Int {
         switch level {
-        case .critical: return 2
+        case .critical: return 0
         case .timeSensitive: return 1
-        case .active: return 0
+        case .active: return 2
         }
     }
 }
@@ -138,11 +140,9 @@ final class TrioModalAlertScheduler: ObservableObject {
 struct TrioAlertBanner: View {
     let alert: LoopKit.Alert
     let onTap: () -> Void
-    let onSwipeAway: () -> Void
     let onSnooze: (TimeInterval) -> Void
 
     @State private var presentedAt = Date()
-    @State private var dragOffset: CGFloat = 0
 
     private var canSnooze: Bool { alert.interruptionLevel != .critical }
 
@@ -215,30 +215,6 @@ struct TrioAlertBanner: View {
         .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 22, style: .continuous))
         .shadow(color: .black.opacity(0.18), radius: 12, y: 4)
         .contentShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
-        .offset(x: dragOffset)
-        .opacity(1 - min(abs(dragOffset) / 200, 0.6))
-        .gesture(
-            DragGesture(minimumDistance: 12)
-                .onChanged { value in
-                    let dx = value.translation.width
-                    dragOffset = abs(dx) > abs(value.translation.height) ? dx : 0
-                }
-                .onEnded { value in
-                    let dx = value.translation.width
-                    // Swipe right past threshold → dismiss / acknowledge.
-                    // Swipe left past threshold → quick snooze (20m) when
-                    // the alert isn't critical. Otherwise reset.
-                    if dx > 80 {
-                        onSwipeAway()
-                    } else if dx < -80, canSnooze {
-                        onSnooze(NotificationResponseAction.snooze20.duration)
-                    } else {
-                        withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
-                            dragOffset = 0
-                        }
-                    }
-                }
-        )
         .onTapGesture { onTap() }
         .contextMenu {
             if canSnooze {
@@ -304,7 +280,6 @@ struct TrioAlertModifier: ViewModifier {
                     TrioAlertBanner(
                         alert: alert,
                         onTap: { scheduler.acknowledge(identifier: alert.identifier) },
-                        onSwipeAway: { scheduler.acknowledge(identifier: alert.identifier) },
                         onSnooze: { duration in
                             scheduler.snooze(identifier: alert.identifier, duration: duration)
                         }
@@ -328,7 +303,6 @@ struct TrioAlertModifier: ViewModifier {
                             isExpanded = true
                         }
                     },
-                    onSwipeAway: { scheduler.acknowledge(identifier: alert.identifier) },
                     onSnooze: { duration in
                         scheduler.snooze(identifier: alert.identifier, duration: duration)
                     }