Przeglądaj źródła

Merge branch 'core-data-sync-trio' of github.com:dnzxy/Trio-dev into settings-update

Deniz Cengiz 1 rok temu
rodzic
commit
0c847f52d7
32 zmienionych plików z 1273 dodań i 319 usunięć
  1. 36 23
      FreeAPS.xcodeproj/project.pbxproj
  2. 38 0
      FreeAPS/Resources/Assets.xcassets/Colors/ApnBackground.colorset/Contents.json
  3. 20 0
      FreeAPS/Resources/Assets.xcassets/Colors/ApnBackgroundLightDark.colorset/Contents.json
  4. 0 10
      FreeAPS/Sources/APS/DeviceDataManager.swift
  5. 0 1
      FreeAPS/Sources/Application/AppDelegate.swift
  6. 1 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  7. 1 1
      FreeAPS/Sources/Assemblies/ServiceAssembly.swift
  8. 20 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  9. 60 0
      FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  10. 10 0
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift
  11. 71 1
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift
  12. 115 27
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  13. 65 0
      FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  14. 250 54
      FreeAPS/Sources/Modules/Main/MainStateModel.swift
  15. 1 1
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift
  16. 7 2
      FreeAPS/Sources/Modules/Settings/SettingItems.swift
  17. 109 1
      FreeAPS/Sources/Modules/Settings/View/Subviews/NotificationsView.swift
  18. 50 0
      FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift
  19. 2 2
      FreeAPS/Sources/Modules/Bolus/BolusDataFlow.swift
  20. 2 2
      FreeAPS/Sources/Modules/Bolus/BolusProvider.swift
  21. 5 5
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  22. 1 1
      FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift
  23. 4 1
      FreeAPS/Sources/Modules/Bolus/View/AddMealPresetView.swift
  24. 44 35
      FreeAPS/Sources/Modules/Bolus/View/MealPresetView.swift
  25. 1 1
      FreeAPS/Sources/Modules/Bolus/View/PopupView.swift
  26. 2 1
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  27. 37 3
      FreeAPS/Sources/Router/Router.swift
  28. 1 1
      FreeAPS/Sources/Router/Screen.swift
  29. 63 0
      FreeAPS/Sources/Services/Notifications/AlertPermissionsChecker.swift
  30. 254 143
      FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift
  31. 1 1
      FreeAPS/Sources/Views/ViewModifiers.swift
  32. 2 2
      README.md

+ 36 - 23
FreeAPS.xcodeproj/project.pbxproj

@@ -7,7 +7,7 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
-		041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */; };
+		041D1E995A6AE92E9289DC49 /* TreatmentsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */; };
 		0437CE46C12535A56504EC19 /* SnoozeRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5822B15939E719628E9FF7C /* SnoozeRootView.swift */; };
 		0D9A5E34A899219C5C4CDFAF /* DataTableStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9455FA2D92E77A6C4AFED8A3 /* DataTableStateModel.swift */; };
 		0F7A65FBD2CD8D6477ED4539 /* GlucoseNotificationSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E625985B47742D498CB1681A /* GlucoseNotificationSettingsProvider.swift */; };
@@ -64,7 +64,7 @@
 		19F95FFA29F1102A00314DDC /* StatRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F95FF929F1102A00314DDC /* StatRootView.swift */; };
 		1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */; };
 		1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60744C3E9BB3652895C908CC /* DataTableProvider.swift */; };
-		23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19984D62EFC0035A9E9644D /* BolusProvider.swift */; };
+		23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19984D62EFC0035A9E9644D /* TreatmentsProvider.swift */; };
 		3083261C4B268E353F36CD0B /* AutotuneConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCCCE633F5E98E41B0CD3C /* AutotuneConfigDataFlow.swift */; };
 		3171D2818C7C72CD1584BB5E /* GlucoseNotificationSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2C6489D29ECCCAD78E0721 /* GlucoseNotificationSettingsStateModel.swift */; };
 		320D030F724170A637F06D50 /* (null) in Sources */ = {isa = PBXBuildFile; };
@@ -283,7 +283,7 @@
 		642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */; };
 		65070A332BFDCB83006F213F /* TidepoolStartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65070A322BFDCB83006F213F /* TidepoolStartView.swift */; };
 		6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */; };
-		69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */; };
+		69A31254F2451C20361D172F /* TreatmentsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223EC0494F55A91E3EA69EF4 /* TreatmentsStateModel.swift */; };
 		69B9A368029F7EB39F525422 /* CarbRatioEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA5E04A2761F6EEA6568E1 /* CarbRatioEditorStateModel.swift */; };
 		6B1A8D192B14D91600E76752 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D182B14D91600E76752 /* WidgetKit.framework */; };
 		6B1A8D1B2B14D91600E76752 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */; };
@@ -298,6 +298,7 @@
 		6EADD581738D64431902AC0A /* (null) in Sources */ = {isa = PBXBuildFile; };
 		6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAE81192B118804DCD23034 /* SnoozeProvider.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
+		71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
 		7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */; };
@@ -341,7 +342,7 @@
 		BDF34F932C10D0E100D51995 /* LiveActivityAttributes+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF34F922C10D0E100D51995 /* LiveActivityAttributes+Helper.swift */; };
 		BDF34F952C10D27300D51995 /* DeterminationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF34F942C10D27300D51995 /* DeterminationData.swift */; };
 		BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */; };
-		BDFD165A2AE40438007F0DDA /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* BolusRootView.swift */; };
+		BDFD165A2AE40438007F0DDA /* TreatmentsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* TreatmentsRootView.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
@@ -721,7 +722,7 @@
 		19F95FF629F10FEE00314DDC /* StatStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatStateModel.swift; sourceTree = "<group>"; };
 		19F95FF929F1102A00314DDC /* StatRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatRootView.swift; sourceTree = "<group>"; };
 		1CAE81192B118804DCD23034 /* SnoozeProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeProvider.swift; sourceTree = "<group>"; };
-		223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusStateModel.swift; sourceTree = "<group>"; };
+		223EC0494F55A91E3EA69EF4 /* TreatmentsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsStateModel.swift; sourceTree = "<group>"; };
 		22963BD06A9C83959D4914E4 /* GlucoseNotificationSettingsRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsRootView.swift; sourceTree = "<group>"; };
 		2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigRootView.swift; sourceTree = "<group>"; };
 		2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -974,6 +975,7 @@
 		6B1A8D252B14D91700E76752 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBridge.swift; sourceTree = "<group>"; };
 		6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActitiyAttributes.swift; sourceTree = "<group>"; };
+		71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertPermissionsChecker.swift; sourceTree = "<group>"; };
 		79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorDataFlow.swift; sourceTree = "<group>"; };
 		7E22146D3DF4853786C78132 /* CarbRatioEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorDataFlow.swift; sourceTree = "<group>"; };
 		8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigRootView.swift; sourceTree = "<group>"; };
@@ -1021,12 +1023,13 @@
 		BDF34F922C10D0E100D51995 /* LiveActivityAttributes+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivityAttributes+Helper.swift"; sourceTree = "<group>"; };
 		BDF34F942C10D27300D51995 /* DeterminationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterminationData.swift; sourceTree = "<group>"; };
 		BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenView.swift; sourceTree = "<group>"; };
-		BDFD16592AE40438007F0DDA /* BolusRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusRootView.swift; sourceTree = "<group>"; };
+		BDFD16592AE40438007F0DDA /* TreatmentsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentsRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantValues.swift; sourceTree = "<group>"; };
+		C19984D62EFC0035A9E9644D /* TreatmentsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsProvider.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
-		C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusDataFlow.swift; sourceTree = "<group>"; };
+		C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsDataFlow.swift; sourceTree = "<group>"; };
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
 		CC76E9502BD4812E008BEB61 /* Forecast+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Forecast+helper.swift"; sourceTree = "<group>"; };
 		CE1856F42ADC4858007E39C7 /* AddCarbPresetIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCarbPresetIntent.swift; sourceTree = "<group>"; };
@@ -1507,20 +1510,17 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
-				DD9ECB6B2CA99FA400AA7C45 /* RemoteControlConfig */,
-				DD9ECB662CA99EFE00AA7C45 /* RemoteControl */,
 				DD1745382C55BF8B00211FAC /* AlgorithmAdvancedSettings */,
 				DD1745422C55C5C400211FAC /* AutosensSettings */,
 				672F63EEAE27400625E14BAD /* AutotuneConfig */,
 				A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */,
 				3811DE0425C9D32E00A708ED /* Base */,
-				C2C98283C436DB934D7E7994 /* Bolus */,
 				BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */,
 				DD09D4792C5986BA003FEA5D /* CalendarEventSettings */,
 				CEE9A64D2BBB411C00EB5194 /* Calibrations */,
+				E42231DBF0DBE2B4B92D1B15 /* CarbRatioEditor */,
 				F75CB57ED6971B46F8756083 /* CGM */,
 				0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */,
-				E42231DBF0DBE2B4B92D1B15 /* CarbRatioEditor */,
 				9E56E3626FAD933385101B76 /* DataTable */,
 				195D80B22AF696EE00D25097 /* DynamicSettings */,
 				DD17454C2C55CA0200211FAC /* GeneralSettings */,
@@ -1536,6 +1536,8 @@
 				D533BF261CDC1C3F871E7BFD /* NightscoutConfig */,
 				DDD163032C4C67B400CD525A /* OverrideConfig */,
 				99C01B871ACAB3F32CE755C7 /* PumpConfig */,
+				DD9ECB662CA99EFE00AA7C45 /* RemoteControl */,
+				DD9ECB6B2CA99FA400AA7C45 /* RemoteControlConfig */,
 				3811DE3825C9D4A100A708ED /* Settings */,
 				110AEDEA2C51A0AE00615CC9 /* ShortcutsConfig */,
 				DD17451E2C55520000211FAC /* SMBSettings */,
@@ -1543,6 +1545,7 @@
 				19F95FF129F10F9C00314DDC /* Stat */,
 				DD17452C2C55AE3500211FAC /* TargetBehavoir */,
 				6517011F19F244F64E1FF14B /* TargetsEditor */,
+				C2C98283C436DB934D7E7994 /* Treatments */,
 				190EBCC229FF134900BA767D /* UserInterfaceSettings */,
 				CE94597C29E9E1CD0047C9C6 /* WatchConfig */,
 			);
@@ -2087,6 +2090,7 @@
 		38B4F3C425E5016800E76A18 /* Notifications */ = {
 			isa = PBXGroup;
 			children = (
+				71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */,
 				38B4F3CC25E5031100E76A18 /* Broadcaster.swift */,
 				38B4F3C525E5017E00E76A18 /* NotificationCenter.swift */,
 				38B4F3C725E502C000E76A18 /* SwiftNotificationCenter */,
@@ -2431,11 +2435,10 @@
 		B9488883C59C31550E0B4CEC /* View */ = {
 			isa = PBXGroup;
 			children = (
-				BDFD16592AE40438007F0DDA /* BolusRootView.swift */,
+				BDFD16592AE40438007F0DDA /* TreatmentsRootView.swift */,
 				58237D9D2BCF0A6B00A47A79 /* PopupView.swift */,
 				BDB899872C564509006F3298 /* ForecastChart.swift */,
-				BD0B2EF22C5998E600B3298F /* MealPresetView.swift */,
-				DDF847E52C5D66490049BB3B /* AddMealPresetView.swift */,
+				DD07CA5A2CE950B9002D45A9 /* MealPreset */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2470,15 +2473,15 @@
 			path = Data;
 			sourceTree = "<group>";
 		};
-		C2C98283C436DB934D7E7994 /* Bolus */ = {
+		C2C98283C436DB934D7E7994 /* Treatments */ = {
 			isa = PBXGroup;
 			children = (
-				C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */,
-				C19984D62EFC0035A9E9644D /* BolusProvider.swift */,
-				223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */,
+				C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */,
+				C19984D62EFC0035A9E9644D /* TreatmentsProvider.swift */,
+				223EC0494F55A91E3EA69EF4 /* TreatmentsStateModel.swift */,
 				B9488883C59C31550E0B4CEC /* View */,
 			);
-			path = Bolus;
+			path = Treatments;
 			sourceTree = "<group>";
 		};
 		CE1856F32ADC4835007E39C7 /* Carbs */ = {
@@ -2605,6 +2608,15 @@
 			path = ISFEditor;
 			sourceTree = "<group>";
 		};
+		DD07CA5A2CE950B9002D45A9 /* MealPreset */ = {
+			isa = PBXGroup;
+			children = (
+				BD0B2EF22C5998E600B3298F /* MealPresetView.swift */,
+				DDF847E52C5D66490049BB3B /* AddMealPresetView.swift */,
+			);
+			path = MealPreset;
+			sourceTree = "<group>";
+		};
 		DD09D4792C5986BA003FEA5D /* CalendarEventSettings */ = {
 			isa = PBXGroup;
 			children = (
@@ -3487,7 +3499,7 @@
 				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
-				BDFD165A2AE40438007F0DDA /* BolusRootView.swift in Sources */,
+				BDFD165A2AE40438007F0DDA /* TreatmentsRootView.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,
 				DD1745522C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift in Sources */,
 				190EBCC429FF136900BA767D /* UserInterfaceSettingsDataFlow.swift in Sources */,
@@ -3631,14 +3643,14 @@
 				E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */,
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
-				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
+				041D1E995A6AE92E9289DC49 /* TreatmentsDataFlow.swift in Sources */,
 				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
-				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
+				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
 				DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
-				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
+				69A31254F2451C20361D172F /* TreatmentsStateModel.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
 				19D4E4EB29FC6A9F00351451 /* Charts.swift in Sources */,
 				FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */,
@@ -3686,6 +3698,7 @@
 				DDF847E42C5C288F0049BB3B /* LiveActivitySettingsRootView.swift in Sources */,
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,
+				71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */,
 				320D030F724170A637F06D50 /* (null) in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				19E1F7E829D082D0005C8D20 /* IconConfigDataFlow.swift in Sources */,

+ 38 - 0
FreeAPS/Resources/Assets.xcassets/Colors/ApnBackground.colorset/Contents.json

@@ -0,0 +1,38 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.929",
+          "green" : "0.926",
+          "red" : "0.918"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "0.950",
+          "blue" : "0.208",
+          "green" : "0.145",
+          "red" : "0.063"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 20 - 0
FreeAPS/Resources/Assets.xcassets/Colors/ApnBackgroundLightDark.colorset/Contents.json

@@ -0,0 +1,20 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "0.950",
+          "blue" : "0.639",
+          "green" : "0.572",
+          "red" : "0.478"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 0 - 10
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -648,15 +648,6 @@ extension BaseDeviceDataManager: AlertObserver {
     }
 
     private func ackAlert(alert: AlertEntry) {
-        let typeMessage: MessageType
-        let alertUp = alert.alertIdentifier.uppercased()
-        if alertUp.contains("FAULT") || alertUp.contains("ERROR") {
-            typeMessage = .errorPump
-        } else {
-            typeMessage = .warning
-        }
-
-        let messageCont = MessageContent(content: alert.contentBody ?? "Unknown", type: typeMessage)
         let alertIssueDate = alert.issuedDate
 
         processQueue.async {
@@ -676,7 +667,6 @@ extension BaseDeviceDataManager: AlertObserver {
             }
 
             self.pumpManager?.acknowledgeAlert(alertIdentifier: alert.alertIdentifier) { error in
-                self.router.alertMessage.send(messageCont)
                 if let error = error {
                     self.alertHistoryStorage.ackAlert(alertIssueDate, error.localizedDescription)
                     debug(.deviceManager, "acknowledge not succeeded with error \(error.localizedDescription)")

+ 0 - 1
FreeAPS/Sources/Application/AppDelegate.swift

@@ -7,7 +7,6 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         _ application: UIApplication,
         didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?
     ) -> Bool {
-        UNUserNotificationCenter.current().delegate = self
         application.registerForRemoteNotifications()
         return true
     }

+ 1 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -51,6 +51,7 @@ import Swinject
         _ = resolver.resolve(HealthKitManager.self)!
         _ = resolver.resolve(BluetoothStateManager.self)!
         _ = resolver.resolve(PluginManager.self)!
+        _ = resolver.resolve(AlertPermissionsChecker.self)!
         if #available(iOS 16.2, *) {
             _ = resolver.resolve(LiveActivityBridge.self)!
         }

+ 1 - 1
FreeAPS/Sources/Assemblies/ServiceAssembly.swift

@@ -20,7 +20,7 @@ final class ServiceAssembly: Assembly {
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
         container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
         container.register(GarminManager.self) { r in BaseGarminManager(resolver: r) }
-
+        container.register(AlertPermissionsChecker.self) { r in AlertPermissionsChecker(resolver: r) }
         if #available(iOS 16.2, *) {
             container.register(LiveActivityBridge.self) { r in
                 LiveActivityBridge(resolver: r)

+ 20 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -33,6 +33,10 @@ struct FreeAPSSettings: JSON, Equatable {
     var displayCalendarIOBandCOB: Bool = false
     var displayCalendarEmojis: Bool = false
     var glucoseBadge: Bool = false
+    var notificationsPump: Bool = true
+    var notificationsCgm: Bool = true
+    var notificationsCarb: Bool = true
+    var notificationsAlgorithm: Bool = true
     var glucoseNotificationsAlways: Bool = false
     var useAlarmSound: Bool = false
     var addSourceInfoToGlucoseNotifications: Bool = false
@@ -204,6 +208,22 @@ extension FreeAPSSettings: Decodable {
             settings.delay = delay
         }
 
+        if let notificationsPump = try? container.decode(Bool.self, forKey: .notificationsPump) {
+            settings.notificationsPump = notificationsPump
+        }
+
+        if let notificationsCgm = try? container.decode(Bool.self, forKey: .notificationsCgm) {
+            settings.notificationsCgm = notificationsCgm
+        }
+
+        if let notificationsCarb = try? container.decode(Bool.self, forKey: .notificationsCarb) {
+            settings.notificationsCarb = notificationsCarb
+        }
+
+        if let notificationsAlgorithm = try? container.decode(Bool.self, forKey: .notificationsAlgorithm) {
+            settings.notificationsAlgorithm = notificationsAlgorithm
+        }
+
         if let glucoseNotificationsAlways = try? container.decode(Bool.self, forKey: .glucoseNotificationsAlways) {
             settings.glucoseNotificationsAlways = glucoseNotificationsAlways
         }

+ 60 - 0
FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -1,3 +1,4 @@
+import Charts
 import SwiftUI
 import Swinject
 
@@ -154,6 +155,7 @@ extension CarbRatioEditor {
 
         private var list: some View {
             List {
+                chart.padding(.vertical)
                 ForEach(state.items.indexed(), id: \.1.id) { index, item in
                     NavigationLink(destination: pickers(for: index)) {
                         HStack {
@@ -174,6 +176,64 @@ extension CarbRatioEditor {
             }
         }
 
+        let chartScale = Calendar.current
+            .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
+
+        var chart: some View {
+            Chart {
+                ForEach(state.items.indexed(), id: \.1.id) { index, item in
+                    let displayValue = state.rateValues[item.rateIndex]
+
+                    let tzOffset = TimeZone.current.secondsFromGMT() * -1
+                    let startDate = Date(timeIntervalSinceReferenceDate: state.timeValues[item.timeIndex])
+                        .addingTimeInterval(TimeInterval(tzOffset))
+                    let endDate = state.items
+                        .count > index + 1 ?
+                        Date(timeIntervalSinceReferenceDate: state.timeValues[state.items[index + 1].timeIndex])
+                        .addingTimeInterval(TimeInterval(tzOffset)) :
+                        Date(timeIntervalSinceReferenceDate: state.timeValues.last!).addingTimeInterval(30 * 60)
+                        .addingTimeInterval(TimeInterval(tzOffset))
+                    RectangleMark(
+                        xStart: .value("start", startDate),
+                        xEnd: .value("end", endDate),
+                        yStart: .value("rate-start", displayValue),
+                        yEnd: .value("rate-end", 0)
+                    ).foregroundStyle(
+                        .linearGradient(
+                            colors: [
+                                Color.insulin.opacity(0.6),
+                                Color.insulin.opacity(0.1)
+                            ],
+                            startPoint: .bottom,
+                            endPoint: .top
+                        )
+                    ).alignsMarkStylesWithPlotArea()
+
+                    LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
+                        .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+
+                    LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
+                        .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+                }
+            }
+            .chartXAxis {
+                AxisMarks(values: .automatic(desiredCount: 6)) { _ in
+                    AxisValueLabel(format: .dateTime.hour())
+                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+                }
+            }
+            .chartXScale(
+                domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
+                    .addingTimeInterval(60 * 60 * 24)
+            )
+            .chartYAxis {
+                AxisMarks(values: .automatic(desiredCount: 4)) { _ in
+                    AxisValueLabel()
+                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+                }
+            }
+        }
+
         private var addButton: some View {
             guard state.canAdd else {
                 return AnyView(EmptyView())

+ 10 - 0
FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift

@@ -9,12 +9,22 @@ extension GlucoseNotificationSettings {
         @Published var lowGlucose: Decimal = 0
         @Published var highGlucose: Decimal = 0
 
+        @Published var notificationsPump = true
+        @Published var notificationsCgm = true
+        @Published var notificationsCarb = true
+        @Published var notificationsAlgorithm = true
+
         var units: GlucoseUnits = .mgdL
 
         override func subscribe() {
             let units = settingsManager.settings.units
             self.units = units
 
+            subscribeSetting(\.notificationsPump, on: $notificationsPump) { notificationsPump = $0 }
+            subscribeSetting(\.notificationsCgm, on: $notificationsCgm) { notificationsCgm = $0 }
+            subscribeSetting(\.notificationsCarb, on: $notificationsCarb) { notificationsCarb = $0 }
+            subscribeSetting(\.notificationsAlgorithm, on: $notificationsAlgorithm) { notificationsAlgorithm = $0 }
+
             subscribeSetting(\.glucoseBadge, on: $glucoseBadge) { glucoseBadge = $0 }
             subscribeSetting(\.glucoseNotificationsAlways, on: $glucoseNotificationsAlways) { glucoseNotificationsAlways = $0 }
             subscribeSetting(\.useAlarmSound, on: $useAlarmSound) { useAlarmSound = $0 }

Plik diff jest za duży
+ 71 - 1
FreeAPS/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift


+ 115 - 27
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -16,6 +16,8 @@ extension Home {
         @State var selectedTab: Int = 0
         @State private var statusTitle: String = ""
         @State var showPumpSelection: Bool = false
+        @State var notificationsDisabled = false
+        @State var alertSafetyNotificationsViewHeight = 0
 
         struct Buttons: Identifiable {
             let label: String
@@ -54,6 +56,8 @@ extension Home {
             sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
         ) var enactedSliderTT: FetchedResults<TempTargetsSlider>
 
+        // TODO: end todo
+
         var bolusProgressFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
@@ -684,41 +688,122 @@ extension Home {
             }
         }
 
-        @ViewBuilder func mainView() -> some View {
+        @ViewBuilder func alertSafetyNotificationsView(geo: GeometryProxy) -> some View {
+            ZStack {
+                /// rectangle as background
+                RoundedRectangle(cornerRadius: 15)
+                    .fill(
+                        Color(
+                            red: 0.9,
+                            green: 0.133333333,
+                            blue: 0.2156862745
+                        )
+                    )
+                    .clipShape(RoundedRectangle(cornerRadius: 15))
+                    .frame(height: geo.size.height * 0.08)
+                    .coordinateSpace(name: "alertSafetyNotificationsView")
+                    .shadow(
+                        color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
+                            Color.black.opacity(0.33),
+                        radius: 3
+                    )
+                HStack {
+                    Spacer()
+                    VStack {
+                        Text("⚠️ Safety Notifications are OFF")
+                            .font(.subheadline)
+                            .font(.system(size: 15, weight: .bold, design: .rounded))
+                            .foregroundStyle(.white.gradient)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                        Text("Fix now by turning Notifications ON.")
+                            .font(.caption)
+                            .font(.system(size: 12, weight: .bold, design: .rounded))
+                            .foregroundStyle(.white.gradient)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                    }.padding(.leading, 5)
+                    Spacer()
+                    Image(systemName: "chevron.right").foregroundColor(.white)
+                        .font(.system(size: 15, design: .rounded))
+                }.padding(.horizontal, 10)
+                    .padding(.trailing, 8)
+                    .onTapGesture {
+                        UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
+                    }
+            }.padding(.horizontal, 10)
+                .padding(.top, 0)
+        }
+
+        @ViewBuilder func mainViewWithScrollView() -> some View {
             GeometryReader { geo in
-                VStack(spacing: 0) {
-                    ZStack {
-                        /// glucose bobble
-                        glucoseView
+                ScrollView(.vertical, showsIndicators: false) {
+                    mainViewViews(geo)
+                }
+            }
+        }
 
-                        /// right panel with loop status and evBG
-                        HStack {
-                            Spacer()
-                            rightHeaderPanel(geo)
-                        }.padding(.trailing, 20)
+        @ViewBuilder func mainViewViews(_ geo: GeometryProxy) -> some View {
+            VStack(spacing: 0) {
+                if notificationsDisabled {
+                    alertSafetyNotificationsView(geo: geo)
+                        .padding(.top, UIDevice.adjustPadding(min: nil, max: 40))
+                }
+                ZStack {
+                    /// glucose bobble
+                    glucoseView
 
-                        /// left panel with pump related info
-                        HStack {
-                            pumpView
-                            Spacer()
-                        }.padding(.leading, 20)
-                    }.padding(.top, 10)
+                    /// right panel with loop status and evBG
+                    HStack {
+                        Spacer()
+                        rightHeaderPanel(geo)
+                    }.padding(.trailing, 20)
+
+                    /// left panel with pump related info
+                    HStack {
+                        pumpView
+                        Spacer()
+                    }.padding(.leading, 20)
+                }.padding(.top, 10)
 
-                    mealPanel(geo).padding(.top, UIDevice.adjustPadding(min: nil, max: 30))
-                        .padding(.bottom, UIDevice.adjustPadding(min: nil, max: 20))
+                mealPanel(geo).padding(.top, UIDevice.adjustPadding(min: nil, max: 30))
+                    .padding(.bottom, UIDevice.adjustPadding(min: nil, max: 20))
 
-                    mainChart(geo: geo)
+                mainChart(geo: geo)
 
-                    timeInterval.padding(.top, UIDevice.adjustPadding(min: 0, max: 12))
-                        .padding(.bottom, UIDevice.adjustPadding(min: 0, max: 12))
+                timeInterval.padding(.top, UIDevice.adjustPadding(min: 0, max: 12))
+                    .padding(.bottom, UIDevice.adjustPadding(min: 0, max: 12))
 
-                    if let progress = state.bolusProgress {
-                        bolusView(geo: geo, progress).padding(.bottom, UIDevice.adjustPadding(min: nil, max: 40))
-                    } else {
-                        profileView(geo: geo).padding(.bottom, UIDevice.adjustPadding(min: nil, max: 40))
+                if let progress = state.bolusProgress {
+                    bolusView(geo: geo, progress)
+                        .padding(.bottom, UIDevice.adjustPadding(min: nil, max: 40))
+                } else {
+                    profileView(geo: geo).padding(.bottom, UIDevice.adjustPadding(min: nil, max: 40))
+                }
+            }
+            .background(color)
+            .onReceive(
+                resolver.resolve(AlertPermissionsChecker.self)!.$notificationsDisabled,
+                perform: {
+                    if notificationsDisabled != $0 {
+                        notificationsDisabled = $0
+                        if notificationsDisabled {
+                            debug(.default, "notificationsDisabled")
+                        }
+                    }
+                }
+            )
+        }
+
+        @ViewBuilder func mainView() -> some View {
+            GeometryReader { geo in
+                if notificationsDisabled {
+                    ScrollView(.vertical, showsIndicators: false) {
+                        mainViewViews(geo)
+                    }
+                } else {
+                    GeometryReader { geo in
+                        mainViewViews(geo)
                     }
                 }
-                .background(color)
             }
             .onChange(of: state.hours) {
                 highlightButtons()
@@ -1021,7 +1106,10 @@ extension UIDevice {
         case largeDevice = 852 // Height for 6.1" iPhone 15 Pro
     }
 
-    @usableFromInline static func adjustPadding(min: CGFloat? = nil, max: CGFloat? = nil) -> CGFloat? {
+    @usableFromInline static func adjustPadding(
+        min: CGFloat? = nil,
+        max: CGFloat? = nil
+    ) -> CGFloat? {
         if UIScreen.screenHeight > UIDevice.DeviceSize.smallDevice.rawValue {
             if UIScreen.screenHeight >= UIDevice.DeviceSize.largeDevice.rawValue {
                 return max

+ 65 - 0
FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -1,3 +1,4 @@
+import Charts
 import SwiftUI
 import Swinject
 
@@ -193,6 +194,7 @@ extension ISFEditor {
 
         private var list: some View {
             List {
+                chart.padding(.vertical)
                 ForEach(state.items.indexed(), id: \.1.id) { index, item in
                     let displayValue = state.units == .mgdL ? state.rateValues[item.rateIndex].description : state
                         .rateValues[item.rateIndex].formattedAsMmolL
@@ -217,6 +219,69 @@ extension ISFEditor {
             }
         }
 
+        let chartScale = Calendar.current
+            .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
+
+        var chart: some View {
+            Chart {
+                ForEach(state.items.indexed(), id: \.1.id) { index, item in
+                    let displayValue = state.units == .mgdL ? state.rateValues[item.rateIndex].description : state
+                        .rateValues[item.rateIndex].formattedAsMmolL
+
+                    // Convert from string so we know we use the same math as the rest of Trio.
+                    // However, swift doesn't understand languages that use comma as decimal delminator
+                    let displayValueFloat = Double(displayValue.replacingOccurrences(of: ",", with: "."))
+
+                    let tzOffset = TimeZone.current.secondsFromGMT() * -1
+                    let startDate = Date(timeIntervalSinceReferenceDate: state.timeValues[item.timeIndex])
+                        .addingTimeInterval(TimeInterval(tzOffset))
+                    let endDate = state.items
+                        .count > index + 1 ?
+                        Date(timeIntervalSinceReferenceDate: state.timeValues[state.items[index + 1].timeIndex])
+                        .addingTimeInterval(TimeInterval(tzOffset)) :
+                        Date(timeIntervalSinceReferenceDate: state.timeValues.last!).addingTimeInterval(30 * 60)
+                        .addingTimeInterval(TimeInterval(tzOffset))
+                    RectangleMark(
+                        xStart: .value("start", startDate),
+                        xEnd: .value("end", endDate),
+                        yStart: .value("rate-start", displayValueFloat ?? 0),
+                        yEnd: .value("rate-end", 0)
+                    ).foregroundStyle(
+                        .linearGradient(
+                            colors: [
+                                Color.insulin.opacity(0.6),
+                                Color.insulin.opacity(0.1)
+                            ],
+                            startPoint: .bottom,
+                            endPoint: .top
+                        )
+                    ).alignsMarkStylesWithPlotArea()
+
+                    LineMark(x: .value("End Date", startDate), y: .value("ISF", displayValueFloat ?? 0))
+                        .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+
+                    LineMark(x: .value("Start Date", endDate), y: .value("ISF", displayValueFloat ?? 0))
+                        .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+                }
+            }
+            .chartXAxis {
+                AxisMarks(values: .automatic(desiredCount: 6)) { _ in
+                    AxisValueLabel(format: .dateTime.hour())
+                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+                }
+            }
+            .chartXScale(
+                domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
+                    .addingTimeInterval(60 * 60 * 24)
+            )
+            .chartYAxis {
+                AxisMarks(values: .automatic(desiredCount: 4)) { _ in
+                    AxisValueLabel()
+                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+                }
+            }
+        }
+
         private var addButton: some View {
             guard state.canAdd else {
                 return AnyView(EmptyView())

+ 250 - 54
FreeAPS/Sources/Modules/Main/MainStateModel.swift

@@ -1,3 +1,4 @@
+import Combine
 import LoopKitUI
 import SwiftMessages
 import SwiftUI
@@ -5,11 +6,201 @@ import Swinject
 
 extension Main {
     final class StateModel: BaseStateModel<Provider> {
+        @Injected() var alertPermissionsChecker: AlertPermissionsChecker!
+        @Injected() var broadcaster: Broadcaster!
         private(set) var modal: Modal?
         @Published var isModalPresented = false
         @Published var isSecondaryModalPresented = false
         @Published var secondaryModalView: AnyView? = nil
 
+        @Persisted(key: "UserNotificationsManager.snoozeUntilDate") private var snoozeUntilDate: Date = .distantPast
+        private var timers: [TimeInterval: Timer] = [:]
+
+        private func showTriggeredView(
+            message: MessageContent,
+            interval _: TimeInterval,
+            config: SwiftMessages.Config,
+            view: MessageView
+        ) {
+            view.customConfigureTheme(
+                colorSchemePreference: colorSchemePreference
+            )
+            setupAction(message: message, view: view)
+
+            SwiftMessages.show(config: config, view: view)
+        }
+
+        // Add or replace timer for a specific TimeInterval
+        private func addOrReplaceTriggerTimer(message: MessageContent, config: SwiftMessages.Config, view: MessageView) {
+            let trigger = message.trigger as! UNTimeIntervalNotificationTrigger
+            guard trigger.timeInterval > 0 else { return }
+            let interval = trigger.timeInterval
+
+            SwiftMessages.hide(id: view.id)
+
+            // If a timer already exists for this interval, invalidate it
+            if let existingTimer = timers[interval] {
+                existingTimer.invalidate()
+            }
+
+            // Create a new timer with the provided interval
+            let newTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
+                self?.showTriggeredView(message: message, interval: interval, config: config, view: view)
+                self?.timers[interval] = nil
+            }
+
+            timers[interval] = newTimer
+        }
+
+        // Cancel all timers (optional cleanup method)
+        private func cancelAllTimers() {
+            timers.values.forEach { $0.invalidate() }
+            timers.removeAll()
+        }
+
+        private func setupPumpConfig() {
+            // display the pump configuration immediatly
+            if let pump = provider.deviceManager.pumpManager,
+               let bluetooth = provider.bluetoothProvider
+            {
+                let view = PumpConfig.PumpSettingsView(
+                    pumpManager: pump,
+                    bluetoothManager: bluetooth,
+                    completionDelegate: self
+                ).asAny()
+                router.mainSecondaryModalView.send(view)
+            }
+        }
+
+        private func setupButton(message _: MessageContent, view: MessageView) {
+            view.button?.setImage(UIImage(), for: .normal)
+            view.iconLabel = nil
+            let buttonImage = UIImage(systemName: "chevron.right")?.withTintColor(.white)
+            view.button?.setImage(buttonImage, for: .normal)
+            view.button?.backgroundColor = view.backgroundView.backgroundColor
+            view.button?.tintColor = view.iconImageView?.tintColor
+        }
+
+        private func setupAction(message: MessageContent, view: MessageView) {
+            switch message.action {
+            case .snooze:
+                setupButton(message: message, view: view)
+                view.buttonTapHandler = { _ in
+                    // Popup Snooze view when user taps on Glucose Notification
+                    SwiftMessages.hide()
+                    self.router.mainModalScreen.send(.snooze)
+                }
+            case .pumpConfig:
+                setupButton(message: message, view: view)
+                view.buttonTapHandler = { _ in
+                    SwiftMessages.hide()
+                    self.setupPumpConfig()
+                }
+            default: // break
+                view.button?.setImage(UIImage(), for: .normal)
+                view.buttonTapHandler = { _ in
+                    SwiftMessages.hide()
+                }
+            }
+        }
+
+        private func isApnPumpConfigAction(_ message: MessageContent) -> Bool {
+            if message.type != .error, message.action == .pumpConfig {
+                setupPumpConfig()
+                return true
+            }
+            return false
+        }
+
+        private func showAlertMessage(_ message: MessageContent) {
+            if message.useAPN, !alertPermissionsChecker.notificationsDisabled
+            {
+                showAPN(message)
+            } else {
+                showSwiftMessage(message)
+            }
+        }
+
+        private func showAPN(_ message: MessageContent) {
+            DispatchQueue.main.async {
+                self.broadcaster.notify(alertMessageNotificationObserver.self, on: .main) {
+                    $0.alertMessageNotification(message)
+                }
+            }
+        }
+
+        // Read the color scheme preference from UserDefaults; defaults to system default setting
+        @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
+
+        private func showSwiftMessage(_ message: MessageContent) {
+            if snoozeUntilDate > Date(), message.action == .snooze {
+                return
+            }
+
+            var config = SwiftMessages.defaultConfig
+            let view = MessageView.viewFromNib(layout: .cardView)
+
+            view.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
+            config.prefersStatusBarHidden = true
+
+            // Set id so that multiple notifications are not queued while waiting for user response; only the latest will be shown
+            if message.subtype == .glucose || message.subtype == .carb {
+                view.id = message.type.rawValue + message.subtype.rawValue
+            }
+
+            let titleContent: String
+
+            let iconName = UIApplication.shared.alternateIconName ?? "trioBlack"
+            let iconImage = UIImage(named: iconName) ?? UIImage()
+
+            view.configureContent(
+                title: "title",
+                body: NSLocalizedString(message.content, comment: "Info message"),
+                iconImage: nil,
+                iconText: nil,
+                buttonImage: nil,
+                buttonTitle: nil,
+                buttonTapHandler: nil
+            )
+
+            view.configureIcon(withSize: CGSize(width: 40, height: 40), contentMode: .scaleAspectFit)
+            view.iconImageView!.image = iconImage
+            view.iconImageView?.layer.cornerRadius = 10
+
+            view.customConfigureTheme(
+                colorSchemePreference: colorSchemePreference
+            )
+
+            view.iconImageView?.image = iconImage
+
+            switch message.type {
+            case .info,
+                 .other:
+                config.duration = .seconds(seconds: 5)
+                titleContent = message.title != "" ? message.title : NSLocalizedString("Info", comment: "Info title")
+            case .warning:
+                config.duration = .forever
+                titleContent = message.title != "" ? message
+                    .title : NSLocalizedString("Warning", comment: "Warning title")
+            case .error:
+                config.duration = .forever
+                titleContent = message.title != "" ? message
+                    .title : NSLocalizedString("Error", comment: "Error title")
+            }
+
+            view.titleLabel?.text = titleContent
+            config.dimMode = .gray(interactive: true)
+
+            setupAction(message: message, view: view)
+            if message.trigger != nil {
+                addOrReplaceTriggerTimer(message: message, config: config, view: view)
+            }
+
+            guard message.type == .error || message.action != .pumpConfig, message.trigger == nil, !view.isHidden else { return }
+
+            SwiftMessages.show(config: config, view: view)
+        }
+
         override func subscribe() {
             router.mainModalScreen
                 .map { $0?.modal(resolver: self.resolver!) }
@@ -31,60 +222,9 @@ extension Main {
             router.alertMessage
                 .receive(on: DispatchQueue.main)
                 .sink { message in
-                    var config = SwiftMessages.defaultConfig
-                    let view = MessageView.viewFromNib(layout: .cardView)
-
-                    let titleContent: String
-
-                    view.configureContent(
-                        title: "title",
-                        body: NSLocalizedString(message.content, comment: "Info message"),
-                        iconImage: nil,
-                        iconText: nil,
-                        buttonImage: nil,
-                        buttonTitle: nil,
-                        buttonTapHandler: nil
-                    )
-
-                    switch message.type {
-                    case .info:
-                        view.backgroundColor = .secondarySystemGroupedBackground
-                        config.duration = .automatic
-
-                        titleContent = NSLocalizedString("Info", comment: "Info title")
-                    case .warning:
-                        view.configureTheme(.warning, iconStyle: .subtle)
-                        config.duration = .forever
-                        view.button?.setImage(Icon.warningSubtle.image, for: .normal)
-                        titleContent = NSLocalizedString("Warning", comment: "Warning title")
-                        view.buttonTapHandler = { _ in
-                            SwiftMessages.hide()
-                        }
-                    case .errorPump:
-                        view.configureTheme(.error, iconStyle: .subtle)
-                        config.duration = .forever
-                        view.button?.setImage(Icon.errorSubtle.image, for: .normal)
-                        titleContent = NSLocalizedString("Error", comment: "Error title")
-                        view.buttonTapHandler = { _ in
-                            SwiftMessages.hide()
-                            // display the pump configuration immediatly
-                            if let pump = self.provider.deviceManager.pumpManager,
-                               let bluetooth = self.provider.bluetoothProvider
-                            {
-                                let view = PumpConfig.PumpSettingsView(
-                                    pumpManager: pump,
-                                    bluetoothManager: bluetooth,
-                                    completionDelegate: self
-                                ).asAny()
-                                self.router.mainSecondaryModalView.send(view)
-                            }
-                        }
-                    }
-
-                    view.titleLabel?.text = titleContent
-                    config.dimMode = .gray(interactive: true)
-
-                    SwiftMessages.show(config: config, view: view)
+                    guard !self.isApnPumpConfigAction(message) else { return }
+                    guard self.router.allowNotify(message, self.settingsManager.settings) else { return }
+                    self.showAlertMessage(message)
                 }
                 .store(in: &lifetime)
 
@@ -107,6 +247,53 @@ extension Main {
     }
 }
 
+extension MessageView {
+    func currentColorScheme() -> ColorScheme {
+        let userInterfaceStyle = UITraitCollection.current.userInterfaceStyle
+        return userInterfaceStyle == .dark ? .dark : .light
+    }
+
+    func customConfigureTheme(colorSchemePreference: ColorSchemeOption) {
+        let defaultSystemColorScheme = currentColorScheme()
+        var backgroundColor = UIColor.systemBackground
+        var foregroundColor = UIColor.white
+        let ApnBackground = UIColor(named: "ApnBackground") ?? UIColor.lightGray
+        let iOSlightTrioDark = UIColor(named: "ApnBackgroundLightDark") ?? UIColor.lightGray
+
+        switch colorSchemePreference {
+        case .systemDefault:
+            backgroundColor = ApnBackground
+            foregroundColor = UIColor.label
+        case .dark:
+            backgroundColor = defaultSystemColorScheme == .light ? iOSlightTrioDark : ApnBackground
+            foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
+        case .light:
+            backgroundColor = defaultSystemColorScheme == .light ? ApnBackground : UIColor.gray
+            foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
+        }
+
+        iconImageView?.tintColor = foregroundColor
+        backgroundView.backgroundColor = backgroundColor
+        titleLabel?.textColor = foregroundColor
+        bodyLabel?.textColor = foregroundColor
+        iconImageView?.isHidden = iconImageView?.image == nil
+
+        backgroundView.layer.cornerRadius = 25
+
+        let adjustedFont = UIFont.systemFont(ofSize: 13.0, weight: .bold)
+        let preferredTitleFont = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: adjustedFont)
+        let preferredBodyFont = UIFont.preferredFontforStyle(forTextStyle: .footnote)
+        // Set the title and body font to the dynamic type sizes
+        titleLabel?.adjustsFontForContentSizeCategory = true
+        titleLabel?.font = preferredTitleFont
+        bodyLabel?.adjustsFontForContentSizeCategory = true
+        bodyLabel?.font = preferredBodyFont
+        // Set custom colors for title and body text
+        titleLabel?.textColor = foregroundColor
+        bodyLabel?.textColor = foregroundColor
+    }
+}
+
 @available(iOS 16.0, *)
 extension Main.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
@@ -114,3 +301,12 @@ extension Main.StateModel: CompletionDelegate {
         router.mainSecondaryModalView.send(nil)
     }
 }
+
+// Extension to convert SwiftUI TextStyle to UIFont
+extension UIFont {
+    static func preferredFontforStyle(forTextStyle: UIFont.TextStyle) -> UIFont {
+        let uiFontMetrics = UIFontMetrics.default
+        let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: forTextStyle)
+        return uiFontMetrics.scaledFont(for: UIFont(descriptor: descriptor, size: 0))
+    }
+}

+ 1 - 1
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift

@@ -28,6 +28,6 @@ extension TrioRemoteControl {
     }
 
     private func isRunningInAPNSProductionEnvironment() -> Bool {
-        return BuildDetails.default.isTestFlightBuild()
+        BuildDetails.default.isTestFlightBuild()
     }
 }

+ 7 - 2
FreeAPS/Sources/Modules/Settings/SettingItems.swift

@@ -208,10 +208,15 @@ enum SettingItems {
     ]
 
     static let notificationItems = [
+        SettingItem(title: "Manage iOS Preferences", view: .notificationSettings),
         SettingItem(
-            title: "Glucose Notifications",
+            title: "Trio Notifications",
             view: .glucoseNotificationSettings,
             searchContents: [
+                "Always Notify Pump",
+                "Always Notify CGM",
+                "Always Notify Carb",
+                "Always Notify Algorithm",
                 "Show Glucose App Badge",
                 "Always Notify Glucose",
                 "Play Alarm Sound",
@@ -219,7 +224,7 @@ enum SettingItems {
                 "Low Glucose Alarm Limit",
                 "High Glucose Alarm Limit"
             ],
-            path: ["Notifications", "Glucose Notifications"]
+            path: ["Notifications", "Trio Notifications"] // Glucose
         ),
         SettingItem(
             title: "Live Activity",

+ 109 - 1
FreeAPS/Sources/Modules/Settings/View/Subviews/NotificationsView.swift

@@ -5,6 +5,7 @@
 //  Created by Deniz Cengiz on 26.07.24.
 //
 import Foundation
+import LoopKitUI
 import SwiftUI
 import Swinject
 
@@ -12,6 +13,13 @@ struct NotificationsView: BaseView {
     let resolver: Resolver
 
     @ObservedObject var state: Settings.StateModel
+    @State var notificationsDisabled = false
+    @State var showAlert = false
+    @State private var shouldDisplayHint: Bool = false
+    @State var hintDetent = PresentationDetent.large
+    @State var selectedVerboseHint: String? =
+        "Notifications give you important Trio information without requiring you to open the app.\n\nKeep these turned ON in your phone’s settings to ensure you receive Trio Notifications, Critical Alerts, and Time Sensitive Notifications."
+    @State var hintLabel: String? = "Manage iOS Preferences"
 
     @Environment(\.colorScheme) var colorScheme
     var color: LinearGradient {
@@ -34,9 +42,43 @@ struct NotificationsView: BaseView {
     var body: some View {
         Form {
             Section(
+                header: Text("Manage iOS Preferences"),
+                content: {
+                    manageNotifications
+                }
+            )
+            Section {
+                VStack {
+                    notificationsEnabledStatus
+                    HStack(alignment: .top) {
+                        Text(
+                            "Notifications give you important Trio information without requiring you to open the app."
+                        )
+                        .font(.footnote)
+                        .foregroundColor(.secondary)
+                        .lineLimit(nil)
+                        Spacer()
+                        Button(
+                            action: {
+                                hintLabel = "Manage iOS Preferences"
+                                selectedVerboseHint =
+                                    "Notifications give you important Trio information without requiring you to open the app.\n\nKeep these turned ON in your phone’s settings to ensure you receive Trio Notifications, Critical Alerts, and Time Sensitive Notifications."
+                                shouldDisplayHint.toggle()
+                            },
+                            label: {
+                                HStack {
+                                    Image(systemName: "questionmark.circle")
+                                }
+                            }
+                        ).buttonStyle(BorderlessButtonStyle())
+                    }.padding(.top)
+                }.padding(.bottom)
+            }.listRowBackground(Color.chart)
+            Section(
                 header: Text("Notification Center"),
                 content: {
-                    Text("Glucose Notifications").navigationLink(to: .glucoseNotificationSettings, from: self)
+                    Text("Trio Notifications")
+                        .navigationLink(to: .glucoseNotificationSettings, from: self)
 
                     if #available(iOS 16.2, *) {
                         Text("Live Activity").navigationLink(to: .liveActivitySettings, from: self)
@@ -47,8 +89,74 @@ struct NotificationsView: BaseView {
             )
             .listRowBackground(Color.chart)
         }
+        .onReceive(
+            resolver.resolve(AlertPermissionsChecker.self)!.$notificationsDisabled,
+            perform: {
+                if notificationsDisabled != $0 {
+                    notificationsDisabled = $0
+                    if notificationsDisabled {
+                        showAlert = true
+                    }
+                }
+            }
+        )
+        .alert(
+            isPresented: self.$showAlert,
+            content: { self.notificationReminder() }
+        )
+        .sheet(isPresented: $shouldDisplayHint) {
+            SettingInputHintView(
+                hintDetent: $hintDetent,
+                shouldDisplayHint: $shouldDisplayHint,
+                hintLabel: hintLabel ?? "",
+                hintText: selectedVerboseHint ?? "",
+                sheetTitle: "Help"
+            )
+        }
         .scrollContentBackground(.hidden).background(color)
         .navigationTitle("Notifications")
         .navigationBarTitleDisplayMode(.automatic)
     }
 }
+
+extension NotificationsView {
+    func notificationReminder() -> Alert {
+        Alert(
+            title: Text("\u{2757} Notifications are Required"),
+            message: Text(
+                "Please authorize notifications by tapping 'Open iOS Settings' > 'Notifications' and enable 'Allow Notifications' for 'Notification Center' and 'Banners' Alerts."
+            ),
+            dismissButton: .default(Text("Ok"))
+        )
+    }
+
+    @ViewBuilder private func onOff(_ val: Bool) -> some View {
+        if val {
+            Text(NSLocalizedString("On", comment: "Notification Setting Status is On"))
+        } else {
+            HStack {
+                Image(systemName: "exclamationmark.triangle.fill").foregroundColor(.critical)
+                Text(NSLocalizedString("Off", comment: "Notification Setting Status is Off"))
+            }
+        }
+    }
+
+    private var manageNotifications: some View {
+        Button(action: { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) }) {
+            HStack {
+                Text(NSLocalizedString("Open iOS Settings", comment: "Manage Permissions in Settings button text"))
+                Spacer()
+                Image(systemName: "chevron.right").foregroundColor(.gray).font(.footnote)
+            }
+        }
+        .accentColor(.primary)
+    }
+
+    private var notificationsEnabledStatus: some View {
+        HStack {
+            Text(NSLocalizedString("Notifications", comment: "Notifications Status text"))
+            Spacer()
+            onOff(!notificationsDisabled)
+        }
+    }
+}

+ 50 - 0
FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift

@@ -1,3 +1,4 @@
+import Charts
 import SwiftUI
 import Swinject
 
@@ -135,6 +136,7 @@ extension TargetsEditor {
 
         private var list: some View {
             List {
+                chart.padding(.vertical)
                 ForEach(state.items.indexed(), id: \.1.id) { index, item in
                     NavigationLink(destination: pickers(for: index)) {
                         HStack {
@@ -156,6 +158,54 @@ extension TargetsEditor {
             }
         }
 
+        let chartScale = Calendar.current
+            .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
+
+        var chart: some View {
+            Chart {
+                ForEach(state.items.indexed(), id: \.1.id) { index, item in
+                    let displayValue = state.units == .mgdL ? state.rateValues[item.lowIndex].description : state
+                        .rateValues[item.lowIndex].formattedAsMmolL
+
+                    // Convert from string so we know we use the same math as the rest of Trio.
+                    // However, swift doesn't understand languages that use comma as decimal delminator
+                    let displayValueFloat = Double(displayValue.replacingOccurrences(of: ",", with: "."))
+
+                    let tzOffset = TimeZone.current.secondsFromGMT() * -1
+                    let startDate = Date(timeIntervalSinceReferenceDate: state.timeValues[item.timeIndex])
+                        .addingTimeInterval(TimeInterval(tzOffset))
+                    let endDate = state.items
+                        .count > index + 1 ?
+                        Date(timeIntervalSinceReferenceDate: state.timeValues[state.items[index + 1].timeIndex])
+                        .addingTimeInterval(TimeInterval(tzOffset)) :
+                        Date(timeIntervalSinceReferenceDate: state.timeValues.last!).addingTimeInterval(30 * 60)
+                        .addingTimeInterval(TimeInterval(tzOffset))
+
+                    LineMark(x: .value("End Date", startDate), y: .value("Target", displayValueFloat ?? 0.0))
+                        .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.green.gradient)
+
+                    LineMark(x: .value("Start Date", endDate), y: .value("Target", displayValueFloat ?? 0.0))
+                        .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.green.gradient)
+                }
+            }
+            .chartXAxis {
+                AxisMarks(values: .automatic(desiredCount: 6)) { _ in
+                    AxisValueLabel(format: .dateTime.hour())
+                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+                }
+            }
+            .chartXScale(
+                domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
+                    .addingTimeInterval(60 * 60 * 24)
+            )
+            .chartYAxis {
+                AxisMarks(values: .automatic(desiredCount: 4)) { _ in
+                    AxisValueLabel()
+                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+                }
+            }.chartYScale(domain: (state.units == .mgdL ? 72 : 4.0) ... (state.units == .mgdL ? 180 : 10))
+        }
+
         private var addButton: some View {
             guard state.canAdd else {
                 return AnyView(EmptyView())

+ 2 - 2
FreeAPS/Sources/Modules/Bolus/BolusDataFlow.swift

@@ -1,8 +1,8 @@
-enum Bolus {
+enum Treatments {
     enum Config {}
 }
 
-protocol BolusProvider: Provider {
+protocol TreatmentsProvider: Provider {
     func getPumpSettings() async -> PumpSettings
     func getBasalProfile() async -> [BasalProfileEntry]
     func getCarbRatios() async -> CarbRatios

+ 2 - 2
FreeAPS/Sources/Modules/Bolus/BolusProvider.swift

@@ -1,5 +1,5 @@
-extension Bolus {
-    final class Provider: BaseProvider, BolusProvider {
+extension Treatments {
+    final class Provider: BaseProvider, TreatmentsProvider {
         func getPumpSettings() async -> PumpSettings {
             await storage.retrieveAsync(OpenAPS.Settings.settings, as: PumpSettings.self)
                 ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))

+ 5 - 5
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -6,7 +6,7 @@ import Observation
 import SwiftUI
 import Swinject
 
-extension Bolus {
+extension Treatments {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() var unlockmanager: UnlockManager!
         @ObservationIgnored @Injected() var apsManager: APSManager!
@@ -598,7 +598,7 @@ extension Bolus {
     }
 }
 
-extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
+extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
     func determinationDidUpdate(_: Determination) {
         guard isActive else {
             debug(.bolusState, "skipping determinationDidUpdate; view not active")
@@ -625,7 +625,7 @@ extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
     }
 }
 
-extension Bolus.StateModel {
+extension Treatments.StateModel {
     private func registerHandlers() {
         coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
@@ -656,7 +656,7 @@ extension Bolus.StateModel {
 
 // MARK: - Setup Glucose and Determinations
 
-extension Bolus.StateModel {
+extension Treatments.StateModel {
     // Glucose
     private func setupGlucoseArray() {
         Task {
@@ -782,7 +782,7 @@ extension Bolus.StateModel {
     }
 }
 
-extension Bolus.StateModel {
+extension Treatments.StateModel {
     @MainActor func updateForecasts(with forecastData: Determination? = nil) async {
         guard isActive else {
             return

+ 1 - 1
FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift

@@ -4,7 +4,7 @@ import Foundation
 import SwiftUI
 
 struct ForecastChart: View {
-    var state: Bolus.StateModel
+    var state: Treatments.StateModel
     @Environment(\.colorScheme) var colorScheme
 
     @State private var startMarker = Date(timeIntervalSinceNow: -4 * 60 * 60)

+ 4 - 1
FreeAPS/Sources/Modules/Bolus/View/AddMealPresetView.swift

@@ -8,6 +8,7 @@ struct AddMealPresetView: View {
     @Binding var presetCarbs: Decimal
     @Binding var presetFat: Decimal
     @Binding var presetProtein: Decimal
+    @Binding var displayFatAndProtein: Bool
     var onSave: () -> Void
     var onCancel: () -> Void
 
@@ -48,7 +49,9 @@ struct AddMealPresetView: View {
 
                 Section {
                     carbsTextField()
-                    proteinAndFat()
+                    if displayFatAndProtein {
+                        proteinAndFat()
+                    }
                 }
                 .listRowBackground(Color.chart)
 

+ 44 - 35
FreeAPS/Sources/Modules/Bolus/View/MealPresetView.swift

@@ -3,7 +3,7 @@ import Foundation
 import SwiftUI
 
 struct MealPresetView: View {
-    @Bindable var state: Bolus.StateModel
+    @Bindable var state: Treatments.StateModel
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.dismiss) var dismiss
     @Environment(\.managedObjectContext) var moc
@@ -86,6 +86,7 @@ struct MealPresetView: View {
                     presetCarbs: $presetCarbs,
                     presetFat: $presetFat,
                     presetProtein: $presetProtein,
+                    displayFatAndProtein: $state.useFPUconversion,
                     onSave: savePreset,
                     onCancel: {
                         showAddNewPresetSheet.toggle()
@@ -161,7 +162,7 @@ struct MealPresetView: View {
             dismiss()
         }
         label: {
-            Text("Add to treatments")
+            Text("Add to Treatments")
                 .font(.headline)
                 .foregroundStyle(Color.white)
                 .frame(maxWidth: .infinity, alignment: .center)
@@ -173,7 +174,7 @@ struct MealPresetView: View {
     }
 
     private var noPresetChosen: Bool {
-        state.selection == nil || carbs == 0 || fat == 0 || protein == 0
+        state.selection == nil || carbs == 0 || (state.useFPUconversion && (fat == 0 || protein == 0))
     }
 
     @ViewBuilder private func dishInfos() -> some View {
@@ -201,29 +202,31 @@ struct MealPresetView: View {
                         }
                     }
 
-                    Group {
-                        Text("Fat: ")
-                            .font(.footnote)
-                            .foregroundStyle(.secondary)
-                        HStack(spacing: 2) {
-                            Text("\(fat as NSNumber, formatter: mealFormatter)")
-                                .font(.footnote)
-                            Text(" g")
+                    if state.useFPUconversion {
+                        Group {
+                            Text("Fat: ")
                                 .font(.footnote)
                                 .foregroundStyle(.secondary)
+                            HStack(spacing: 2) {
+                                Text("\(fat as NSNumber, formatter: mealFormatter)")
+                                    .font(.footnote)
+                                Text(" g")
+                                    .font(.footnote)
+                                    .foregroundStyle(.secondary)
+                            }
                         }
-                    }
 
-                    Group {
-                        Text("Protein: ")
-                            .font(.footnote)
-                            .foregroundStyle(.secondary)
-                        HStack(spacing: 2) {
-                            Text("\(protein as NSNumber, formatter: mealFormatter)")
-                                .font(.footnote)
-                            Text(" g")
+                        Group {
+                            Text("Protein: ")
                                 .font(.footnote)
                                 .foregroundStyle(.secondary)
+                            HStack(spacing: 2) {
+                                Text("\(protein as NSNumber, formatter: mealFormatter)")
+                                    .font(.footnote)
+                                Text(" g")
+                                    .font(.footnote)
+                                    .foregroundStyle(.secondary)
+                            }
                         }
                     }
                 }
@@ -266,17 +269,19 @@ struct MealPresetView: View {
                 carbs -= (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal)
             } else { carbs = 0 }
 
-            if fat != 0,
-               (fat - (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
-            {
-                fat -= (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal)
-            } else { fat = 0 }
-
-            if protein != 0,
-               (protein - (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
-            {
-                protein -= (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal)
-            } else { protein = 0 }
+            if state.useFPUconversion {
+                if fat != 0,
+                   (fat - (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
+                {
+                    fat -= (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal)
+                } else { fat = 0 }
+
+                if protein != 0,
+                   (protein - (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
+                {
+                    protein -= (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal)
+                } else { protein = 0 }
+            }
 
             state.removePresetFromNewMeal()
             if carbs == 0, fat == 0, protein == 0 { state.summation = [] }
@@ -299,8 +304,10 @@ struct MealPresetView: View {
     private var plusButton: some View {
         Button {
             carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
-            fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
-            protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
+            if state.useFPUconversion {
+                fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
+                protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
+            }
 
             state.addPresetToNewMeal()
         }
@@ -316,9 +323,11 @@ struct MealPresetView: View {
         if dish != "" {
             let preset = MealPresetStored(context: moc)
             preset.dish = dish
-            preset.fat = presetFat as NSDecimalNumber
-            preset.protein = presetProtein as NSDecimalNumber
             preset.carbs = presetCarbs as NSDecimalNumber
+            if state.useFPUconversion {
+                preset.fat = presetFat as NSDecimalNumber
+                preset.protein = presetProtein as NSDecimalNumber
+            }
 
             do {
                 guard moc.hasChanges else { return }

+ 1 - 1
FreeAPS/Sources/Modules/Bolus/View/PopupView.swift

@@ -1,7 +1,7 @@
 import SwiftUI
 
 struct PopupView: View {
-    var state: Bolus.StateModel
+    var state: Treatments.StateModel
     @Environment(\.colorScheme) var colorScheme
 
     private var fractionDigits: Int {

+ 2 - 1
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -4,7 +4,7 @@ import LoopKitUI
 import SwiftUI
 import Swinject
 
-extension Bolus {
+extension Treatments {
     struct RootView: BaseView {
         enum FocusedField {
             case carbs
@@ -393,6 +393,7 @@ extension Bolus {
             .disabled(disableTaskButton)
             .listRowBackground(
                 limitExceeded ? Color(.systemRed) :
+                    disableTaskButton ? Color(.systemGray) :
                     Color(.systemBlue)
             )
             .shadow(radius: 3)

+ 37 - 3
FreeAPS/Sources/Router/Router.swift

@@ -2,15 +2,30 @@ import Combine
 import SwiftUI
 import Swinject
 
-enum MessageType {
+enum MessageType: String {
     case info
     case warning
-    case errorPump
+    case error
+    case other
+}
+
+enum MessageSubtype: String {
+    case pump
+    case cgm
+    case carb
+    case glucose
+    case algorithm
+    case misc
 }
 
 struct MessageContent {
     var content: String
     var type: MessageType = .info
+    var subtype: MessageSubtype = .misc
+    var title: String = ""
+    var useAPN: Bool = true
+    var trigger: UNNotificationTrigger? = nil
+    var action: NotificationAction = .none
 }
 
 protocol Router {
@@ -18,13 +33,13 @@ protocol Router {
     var mainSecondaryModalView: CurrentValueSubject<AnyView?, Never> { get }
     var alertMessage: PassthroughSubject<MessageContent, Never> { get }
     func view(for screen: Screen) -> AnyView
+    func allowNotify(_ message: MessageContent, _ settings: FreeAPSSettings) -> Bool
 }
 
 final class BaseRouter: Router {
     let mainModalScreen = CurrentValueSubject<Screen?, Never>(nil)
     let mainSecondaryModalView = CurrentValueSubject<AnyView?, Never>(nil)
     let alertMessage = PassthroughSubject<MessageContent, Never>()
-
     private let resolver: Resolver
 
     init(resolver: Resolver) {
@@ -34,4 +49,23 @@ final class BaseRouter: Router {
     func view(for screen: Screen) -> AnyView {
         screen.view(resolver: resolver).asAny()
     }
+
+    func allowNotify(_ message: MessageContent, _ settings: FreeAPSSettings) -> Bool {
+        if message.type == .error { return true }
+        switch message.subtype {
+        case .pump:
+            guard settings.notificationsPump else { return false }
+        case .cgm:
+            guard settings.notificationsCgm else { return false }
+        case .carb:
+            guard settings.notificationsCarb else { return false }
+        case .glucose:
+            guard settings.glucoseNotificationsAlways else { return false }
+        case .algorithm:
+            guard settings.notificationsAlgorithm else { return false }
+        case .misc:
+            return true
+        }
+        return true
+    }
 }

+ 1 - 1
FreeAPS/Sources/Router/Screen.swift

@@ -83,7 +83,7 @@ extension Screen {
         case .targetsEditor:
             TargetsEditor.RootView(resolver: resolver)
         case .bolus:
-            Bolus.RootView(resolver: resolver)
+            Treatments.RootView(resolver: resolver)
         case .manualTempBasal:
             ManualTempBasal.RootView(resolver: resolver)
         case .autotuneConfig:

+ 63 - 0
FreeAPS/Sources/Services/Notifications/AlertPermissionsChecker.swift

@@ -0,0 +1,63 @@
+import Combine
+import Foundation
+import LoopKit
+import SwiftUI
+import Swinject
+
+public class AlertPermissionsChecker: ObservableObject, Injectable {
+    private lazy var cancellables = Set<AnyCancellable>()
+    private var listeningToNotificationCenter = false
+
+    @Published var notificationsDisabled: Bool = false
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+
+        Foundation.NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
+            .sink { [weak self] _ in
+                self?.check()
+            }
+            .store(in: &cancellables)
+
+        Foundation.NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
+            .sink { [weak self] _ in
+                self?.check()
+            }
+            .store(in: &cancellables)
+    }
+
+    func checkNow() {
+        check {
+            // Note: we do this, instead of calling notificationCenterSettingsChanged directly, so that we only
+            // get called when it _changes_.
+            self.listenToNotificationCenter()
+        }
+    }
+
+    private func check(then completion: (() -> Void)? = nil) {
+        UNUserNotificationCenter.current().getNotificationSettings { settings in
+            DispatchQueue.main.async {
+                self.notificationsDisabled = settings.alertSetting == .disabled
+                completion?()
+            }
+        }
+    }
+}
+
+extension AlertPermissionsChecker {
+    private func listenToNotificationCenter() {
+        if !listeningToNotificationCenter {
+            $notificationsDisabled
+                .receive(on: RunLoop.main)
+                .removeDuplicates()
+                .sink(receiveValue: notificationCenterSettingsChanged)
+                .store(in: &cancellables)
+            listeningToNotificationCenter = true
+        }
+    }
+
+    private func notificationCenterSettingsChanged(_: Bool) {
+        // TODO: Add processing for other actions in delegate AlertManager, InAppAlertScheduler, etc., from Loop
+        debug(.default, "notificationCenterSettingsChanged")
+    }
+}

+ 254 - 143
FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -20,12 +20,18 @@ enum NotificationAction: String {
     static let key = "action"
 
     case snooze
+    case pumpConfig
+    case none
 }
 
 protocol BolusFailureObserver {
     func bolusDidFail()
 }
 
+protocol alertMessageNotificationObserver {
+    func alertMessageNotification(_ message: MessageContent)
+}
+
 protocol pumpNotificationObserver {
     func pumpNotification(alert: AlertEntry)
     func pumpRemoveNotification()
@@ -33,14 +39,16 @@ protocol pumpNotificationObserver {
 
 final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, Injectable {
     private enum Identifier: String {
-        case glucocoseNotification = "FreeAPS.glucoseNotification"
+        case glucoseNotification = "FreeAPS.glucoseNotification"
         case carbsRequiredNotification = "FreeAPS.carbsRequiredNotification"
         case noLoopFirstNotification = "FreeAPS.noLoopFirstNotification"
         case noLoopSecondNotification = "FreeAPS.noLoopSecondNotification"
         case bolusFailedNotification = "FreeAPS.bolusFailedNotification"
         case pumpNotification = "FreeAPS.pumpNotification"
+        case alertMessageNotification = "FreeAPS.alertMessageNotification"
     }
 
+    @Injected() var alertPermissionsChecker: AlertPermissionsChecker!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var glucoseStorage: GlucoseStorage!
@@ -60,6 +68,9 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
     private var subscriptions = Set<AnyCancellable>()
 
+    let firstInterval = 20 // min
+    let secondInterval = 40 // min
+
     init(resolver: Resolver) {
         super.init()
         center.delegate = self
@@ -74,6 +85,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         broadcaster.register(DeterminationObserver.self, observer: self)
         broadcaster.register(BolusFailureObserver.self, observer: self)
         broadcaster.register(pumpNotificationObserver.self, observer: self)
+        broadcaster.register(alertMessageNotificationObserver.self, observer: self)
         requestNotificationPermissionsIfNeeded()
         Task {
             await sendGlucoseNotification()
@@ -116,7 +128,12 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private func addAppBadge(glucose: Int?) {
         guard let glucose = glucose, settingsManager.settings.glucoseBadge else {
             DispatchQueue.main.async {
-                UIApplication.shared.applicationIconBadgeNumber = 0
+                self.center.setBadgeCount(-1) { error in
+                    guard let error else {
+                        return
+                    }
+                    print(error)
+                }
             }
             return
         }
@@ -129,97 +146,97 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         }
 
         DispatchQueue.main.async {
-            UIApplication.shared.applicationIconBadgeNumber = badge
+            self.center.setBadgeCount(badge) { error in
+                guard let error else {
+                    return
+                }
+                print(error)
+            }
         }
     }
 
     private func notifyCarbsRequired(_ carbs: Int) {
         guard Decimal(carbs) >= settingsManager.settings.carbsRequiredThreshold,
-              settingsManager.settings.showCarbsRequiredBadge else { return }
-
-        ensureCanSendNotification {
-            var titles: [String] = []
+              settingsManager.settings.showCarbsRequiredBadge, settingsManager.settings.notificationsCarb else { return }
 
-            let content = UNMutableNotificationContent()
+        var titles: [String] = []
 
-            if self.snoozeUntilDate > Date() {
-                titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
-            } else {
-                content.sound = .default
-                self.playSoundIfNeeded()
-            }
+        let content = UNMutableNotificationContent()
 
-            titles.append(String(format: NSLocalizedString("Carbs required: %d g", comment: "Carbs required"), carbs))
+        if snoozeUntilDate > Date() {
+            titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
+        } else {
+            content.sound = .default
+            playSoundIfNeeded()
+        }
 
-            content.title = titles.joined(separator: " ")
-            content.body = String(
-                format: NSLocalizedString(
-                    "To prevent LOW required %d g of carbs",
-                    comment: "To prevent LOW required %d g of carbs"
-                ),
-                carbs
-            )
+        titles.append(String(format: NSLocalizedString("Carbs required: %d g", comment: "Carbs required"), carbs))
 
-            self.addRequest(identifier: .carbsRequiredNotification, content: content, deleteOld: true)
-        }
+        content.title = titles.joined(separator: " ")
+        content.body = String(
+            format: NSLocalizedString(
+                "To prevent LOW required %d g of carbs",
+                comment: "To prevent LOW required %d g of carbs"
+            ),
+            carbs
+        )
+        addRequest(identifier: .carbsRequiredNotification, content: content, deleteOld: true, messageSubtype: .carb)
     }
 
     private func scheduleMissingLoopNotifiactions(date _: Date) {
-        ensureCanSendNotification {
-            let title = NSLocalizedString("Trio Not Active", comment: "Trio Not Active")
-            let body = NSLocalizedString("Last loop was more than %d min ago", comment: "Last loop was more than %d min ago")
-
-            let firstInterval = 20 // min
-            let secondInterval = 40 // min
-
-            let firstContent = UNMutableNotificationContent()
-            firstContent.title = title
-            firstContent.body = String(format: body, firstInterval)
-            firstContent.sound = .default
-
-            let secondContent = UNMutableNotificationContent()
-            secondContent.title = title
-            secondContent.body = String(format: body, secondInterval)
-            secondContent.sound = .default
-
-            let firstTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60 * TimeInterval(firstInterval), repeats: false)
-            let secondTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60 * TimeInterval(secondInterval), repeats: false)
-
-            self.addRequest(
-                identifier: .noLoopFirstNotification,
-                content: firstContent,
-                deleteOld: true,
-                trigger: firstTrigger
-            )
-            self.addRequest(
-                identifier: .noLoopSecondNotification,
-                content: secondContent,
-                deleteOld: true,
-                trigger: secondTrigger
-            )
-        }
+        let title = NSLocalizedString("Trio Not Active", comment: "Trio Not Active")
+        let body = NSLocalizedString("Last loop was more than %d min ago", comment: "Last loop was more than %d min ago")
+
+        let firstContent = UNMutableNotificationContent()
+        firstContent.title = title
+        firstContent.body = String(format: body, firstInterval)
+        firstContent.sound = .default
+
+        let secondContent = UNMutableNotificationContent()
+        secondContent.title = title
+        secondContent.body = String(format: body, secondInterval)
+        secondContent.sound = .default
+
+        let firstTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60 * TimeInterval(firstInterval), repeats: false)
+        let secondTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60 * TimeInterval(secondInterval), repeats: false)
+
+        addRequest(
+            identifier: .noLoopFirstNotification,
+            content: firstContent,
+            deleteOld: true,
+            trigger: firstTrigger,
+            messageType: .error,
+            messageSubtype: .algorithm
+        )
+        addRequest(
+            identifier: .noLoopSecondNotification,
+            content: secondContent,
+            deleteOld: true,
+            trigger: secondTrigger,
+            messageType: .error,
+            messageSubtype: .algorithm
+        )
     }
 
     private func notifyBolusFailure() {
-        ensureCanSendNotification {
-            let title = NSLocalizedString("Bolus failed", comment: "Bolus failed")
-            let body = NSLocalizedString(
-                "Bolus failed or inaccurate. Check pump history before repeating.",
-                comment: "Bolus failed or inaccurate. Check pump history before repeating."
-            )
-
-            let content = UNMutableNotificationContent()
-            content.title = title
-            content.body = body
-            content.sound = .default
-
-            self.addRequest(
-                identifier: .noLoopFirstNotification,
-                content: content,
-                deleteOld: true,
-                trigger: nil
-            )
-        }
+        let title = NSLocalizedString("Bolus failed", comment: "Bolus failed")
+        let body = NSLocalizedString(
+            "Bolus failed or inaccurate. Check pump history before repeating.",
+            comment: "Bolus failed or inaccurate. Check pump history before repeating."
+        )
+        let content = UNMutableNotificationContent()
+        content.title = title
+        content.body = body
+        content.sound = .default
+
+        addRequest(
+            identifier: .noLoopFirstNotification,
+            content: content,
+            deleteOld: true,
+            trigger: nil,
+            messageType: .error,
+            messageSubtype: .pump
+        )
     }
 
     private func fetchGlucoseIDs() async -> [NSManagedObjectID] {
@@ -255,45 +272,53 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
 
             guard glucoseStorage.alarm != nil || settingsManager.settings.glucoseNotificationsAlways else { return }
 
-            ensureCanSendNotification {
-                var titles: [String] = []
-                var notificationAlarm = false
-
-                switch self.glucoseStorage.alarm {
-                case .none:
-                    titles.append(NSLocalizedString("Glucose", comment: "Glucose"))
-                case .low:
-                    titles.append(NSLocalizedString("LOWALERT!", comment: "LOWALERT!"))
-                    notificationAlarm = true
-                case .high:
-                    titles.append(NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!"))
-                    notificationAlarm = true
-                }
+            var titles: [String] = []
+            var notificationAlarm = false
+            var messageType = MessageType.info
+
+            switch glucoseStorage.alarm {
+            case .none:
+                titles.append(NSLocalizedString("Glucose", comment: "Glucose"))
+            case .low:
+                titles.append(NSLocalizedString("LOWALERT!", comment: "LOWALERT!"))
+                messageType = MessageType.warning
+                notificationAlarm = true
+            case .high:
+                titles.append(NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!"))
+                messageType = MessageType.warning
+                notificationAlarm = true
+            }
 
-                let delta = glucoseObjects.count >= 2 ? lastReading - secondLastReading : nil
-                let body = self.glucoseText(
-                    glucoseValue: Int(lastReading),
-                    delta: Int(delta ?? 0),
-                    direction: lastDirection
-                ) + self.infoBody()
-
-                if self.snoozeUntilDate > Date() {
-                    titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
-                    notificationAlarm = false
-                } else {
-                    titles.append(body)
-                    let content = UNMutableNotificationContent()
-                    content.title = titles.joined(separator: " ")
-                    content.body = body
-
-                    if notificationAlarm {
-                        self.playSoundIfNeeded()
-                        content.sound = .default
-                        content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue
-                    }
+            let delta = glucoseObjects.count >= 2 ? lastReading - secondLastReading : nil
+            let body = glucoseText(
+                glucoseValue: Int(lastReading),
+                delta: Int(delta ?? 0),
+                direction: lastDirection
+            ) + infoBody()
 
-                    self.addRequest(identifier: .glucocoseNotification, content: content, deleteOld: true)
+            if snoozeUntilDate > Date() {
+                titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
+                notificationAlarm = false
+            } else {
+                titles.append(body)
+                let content = UNMutableNotificationContent()
+                content.title = titles.joined(separator: " ")
+                content.body = body
+
+                if notificationAlarm {
+                    playSoundIfNeeded()
+                    content.sound = .default
+                    content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue
                 }
+
+                addRequest(
+                    identifier: .glucoseNotification,
+                    content: content,
+                    deleteOld: true,
+                    messageType: messageType,
+                    messageSubtype: .glucose,
+                    action: NotificationAction.snooze
+                )
             }
         } catch {
             debugPrint(
@@ -378,31 +403,39 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         }
     }
 
-    private func ensureCanSendNotification(_ completion: @escaping () -> Void) {
-        center.getNotificationSettings { settings in
-            guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional else {
-                warning(.service, "ensureCanSendNotification failed, authorization denied")
-                return
-            }
-
-            debug(.service, "Sending notification was allowed")
-
-            completion()
-        }
-    }
-
     private func addRequest(
         identifier: Identifier,
         content: UNMutableNotificationContent,
         deleteOld: Bool = false,
-        trigger: UNNotificationTrigger? = nil
+        trigger: UNNotificationTrigger? = nil,
+        messageType: MessageType = MessageType.other,
+        messageSubtype: MessageSubtype = MessageSubtype.misc,
+        action: NotificationAction = NotificationAction.none
     ) {
-        let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: trigger)
+        let messageCont = MessageContent(
+            content: content.body,
+            type: messageType,
+            subtype: messageSubtype,
+            title: content.title,
+            useAPN: false,
+            trigger: trigger,
+            action: action
+        )
+        if alertPermissionsChecker.notificationsDisabled {
+            router.alertMessage.send(messageCont)
+            return
+        }
+        guard router.allowNotify(messageCont, settingsManager.settings) else { return }
+
+        var alertIdentifier = identifier.rawValue
+        alertIdentifier = identifier == .pumpNotification ? alertIdentifier + content
+            .title : (identifier == .alertMessageNotification ? alertIdentifier + content.body : alertIdentifier)
+        let request = UNNotificationRequest(identifier: alertIdentifier, content: content, trigger: trigger)
 
         if deleteOld {
             DispatchQueue.main.async {
-                self.center.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue])
-                self.center.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue])
+                self.center.removeDeliveredNotifications(withIdentifiers: [alertIdentifier])
+                self.center.removePendingNotificationRequests(withIdentifiers: [alertIdentifier])
             }
         }
 
@@ -413,7 +446,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
                     return
                 }
 
-                debug(.service, "Sending \(identifier) notification")
+                debug(.service, "Sending \(identifier) notification for \(request.content.title)")
             }
         }
     }
@@ -463,20 +496,88 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     }
 }
 
+extension BaseUserNotificationsManager: alertMessageNotificationObserver {
+    func alertMessageNotification(_ message: MessageContent) {
+        let content = UNMutableNotificationContent()
+        var identifier: Identifier = .alertMessageNotification
+
+        if message.title == "" {
+            switch message.type {
+            case .info:
+                content.title = NSLocalizedString("Info", comment: "Info title")
+            case .warning:
+                content.title = NSLocalizedString("Warning", comment: "Warning title")
+            case .error:
+                content.title = NSLocalizedString("Error", comment: "Error title")
+            default:
+                content.title = message.title
+            }
+        } else {
+            content.title = message.title
+        }
+        switch message.subtype {
+        case .pump:
+            identifier = .pumpNotification
+        case .carb:
+            identifier = .carbsRequiredNotification
+        case .glucose:
+            identifier = .glucoseNotification
+        case .algorithm:
+            if message.trigger != nil {
+                identifier = message.content.contains(String(firstInterval)) ? Identifier.noLoopFirstNotification : Identifier
+                    .noLoopSecondNotification
+            } else {
+                identifier = Identifier.alertMessageNotification
+            }
+        default:
+            identifier = .alertMessageNotification
+        }
+        switch message.action {
+        case .snooze:
+            content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue
+        case .pumpConfig:
+            content.userInfo[NotificationAction.key] = NotificationAction.pumpConfig.rawValue
+        default: break
+        }
+
+        content.body = NSLocalizedString(message.content, comment: "Info message")
+        content.sound = .default
+        addRequest(
+            identifier: identifier,
+            content: content,
+            deleteOld: true,
+            trigger: message.trigger,
+            messageType: message.type,
+            messageSubtype: message.subtype,
+            action: message.action
+        )
+    }
+}
+
 extension BaseUserNotificationsManager: pumpNotificationObserver {
     func pumpNotification(alert: AlertEntry) {
-        ensureCanSendNotification {
-            let content = UNMutableNotificationContent()
-            content.title = alert.contentTitle ?? "Unknown"
-            content.body = alert.contentBody ?? "Unknown"
-            content.sound = .default
-            self.addRequest(
-                identifier: .pumpNotification,
-                content: content,
-                deleteOld: true,
-                trigger: nil
-            )
+        let content = UNMutableNotificationContent()
+        let alertUp = alert.alertIdentifier.uppercased()
+        let typeMessage: MessageType
+        if alertUp.contains("FAULT") || alertUp.contains("ERROR") {
+            content.userInfo[NotificationAction.key] = NotificationAction.pumpConfig.rawValue
+            typeMessage = .error
+        } else {
+            typeMessage = .warning
+            guard settingsManager.settings.notificationsPump else { return }
         }
+        content.title = alert.contentTitle ?? "Unknown"
+        content.body = alert.contentBody ?? "Unknown"
+        content.sound = .default
+        addRequest(
+            identifier: .pumpNotification,
+            content: content,
+            deleteOld: true,
+            trigger: nil,
+            messageType: typeMessage,
+            messageSubtype: .pump,
+            action: .pumpConfig
+        )
     }
 
     func pumpRemoveNotification() {
@@ -507,7 +608,7 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate {
         willPresent _: UNNotification,
         withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
     ) {
-        completionHandler([.banner, .badge, .sound])
+        completionHandler([.banner, .badge, .sound, .list])
     }
 
     func userNotificationCenter(
@@ -523,6 +624,16 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate {
         switch action {
         case .snooze:
             router.mainModalScreen.send(.snooze)
+        case .pumpConfig:
+            let messageCont = MessageContent(
+                content: response.notification.request.content.body,
+                type: MessageType.other,
+                subtype: .pump,
+                useAPN: false,
+                action: .pumpConfig
+            )
+            router.alertMessage.send(messageCont)
+        default: break
         }
     }
 }

+ 1 - 1
FreeAPS/Sources/Views/ViewModifiers.swift

@@ -180,7 +180,7 @@ extension Backport {
     }
 
     @ViewBuilder func chartForegroundStyleScale(state: any StateModel) -> some View {
-        if (state as? Bolus.StateModel)?.forecastDisplayType == ForecastDisplayType.lines ||
+        if (state as? Treatments.StateModel)?.forecastDisplayType == ForecastDisplayType.lines ||
             (state as? Home.StateModel)?.forecastDisplayType == ForecastDisplayType.lines
         {
             let modifiedContent = content

+ 2 - 2
README.md

@@ -16,7 +16,7 @@ You can either use the Build Script or you can run each command manually.
 
 ### Build Script:
 
-If you copy, paste, and run the following script in Terminal, it will guide you through downloading and installing Trio. More information about the script can be found [here](https://docs.diy-trio.org/en/latest/operate/build.html#build-trio-with-script).
+If you copy, paste, and run the following script in Terminal, it will guide you through downloading and installing Trio. More information about the script can be found [here](https://docs.diy-trio.org/operate/build/#build-trio-with-script).
 
 ```
 /bin/bash -c "$(curl -fsSL \
@@ -65,7 +65,7 @@ Instructions in greater detail, but not Trio-specific:
 
 [Discord Trio - Server ](http://discord.diy-trio.org)
 
-[Trio documentation](https://docs.diy-trio.org/en/latest/)
+[Trio documentation](https://docs.diy-trio.org/)
 
 TODO: Add link: Trio Website (under development, not existing yet)