MainStateModel.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import Combine
  2. import LoopKitUI
  3. import SwiftMessages
  4. import SwiftUI
  5. import Swinject
  6. extension Main {
  7. final class StateModel: BaseStateModel<Provider> {
  8. @Injected() var alertPermissionsChecker: AlertPermissionsChecker!
  9. @Injected() var broadcaster: Broadcaster!
  10. private(set) var modal: Modal?
  11. @Published var isModalPresented = false
  12. @Published var isSecondaryModalPresented = false
  13. @Published var secondaryModalView: AnyView? = nil
  14. @Persisted(key: "UserNotificationsManager.snoozeUntilDate") private var snoozeUntilDate: Date = .distantPast
  15. private var timers: [TimeInterval: Timer] = [:]
  16. private var formatter: DateComponentsFormatter { // TODO: Remove debug only
  17. let formatter = DateComponentsFormatter()
  18. formatter.allowsFractionalUnits = false
  19. formatter.unitsStyle = .full
  20. return formatter
  21. }
  22. private var dateFormatter: DateFormatter { // TODO: Remove debug only
  23. let formatter = DateFormatter()
  24. formatter.timeStyle = .short
  25. return formatter
  26. }
  27. private func formatInterval(_ interval: TimeInterval) -> String { // TODO: Remove debug only
  28. formatter.string(from: interval)!
  29. }
  30. private func showTriggeredView(
  31. message: MessageContent,
  32. interval: TimeInterval,
  33. config: SwiftMessages.Config,
  34. view: MessageView
  35. ) {
  36. let snoozeFor = formatter.string(from: interval)! // TODO: Remove debug only
  37. let untilDate = Date() + interval
  38. debug(
  39. .default,
  40. "Notification triggered for: \n \(String(describing: view.titleLabel?.text)) \(String(describing: view.bodyLabel?.text)) snoozed for \(snoozeFor) until \(dateFormatter.string(from: untilDate))"
  41. )
  42. view.customConfigureTheme(
  43. colorSchemePreference: colorSchemePreference
  44. )
  45. setupAction(message: message, view: view)
  46. SwiftMessages.show(config: config, view: view)
  47. }
  48. // Add or replace timer for a specific TimeInterval
  49. private func addOrReplaceTriggerTimer(message: MessageContent, config: SwiftMessages.Config, view: MessageView) {
  50. let trigger = message.trigger as! UNTimeIntervalNotificationTrigger
  51. guard trigger.timeInterval > 0 else { return }
  52. let interval = trigger.timeInterval
  53. // let interval = message.content.contains("20") ? TimeInterval(60) :
  54. // TimeInterval(120) // TimeInterval(60) // trigger.timeInterval // TODO: remove 60 secs for test
  55. let snoozeFor = formatter.string(from: interval)! // TODO: Remove debug only
  56. let untilDate = Date() + interval
  57. debug(
  58. .default,
  59. "\(message.title) \(message.content) \(message.type) \(message.subtype) will snooze for \(snoozeFor) until \(dateFormatter.string(from: untilDate)) for view.id \(view.id)"
  60. )
  61. SwiftMessages.hide(id: view.id)
  62. // If a timer already exists for this interval, invalidate it
  63. if let existingTimer = timers[interval] {
  64. existingTimer.invalidate()
  65. }
  66. // Create a new timer with the provided interval
  67. let newTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
  68. self?.showTriggeredView(message: message, interval: interval, config: config, view: view)
  69. self?.timers[interval] = nil
  70. }
  71. timers[interval] = newTimer
  72. }
  73. // Cancel all timers (optional cleanup method)
  74. private func cancelAllTimers() {
  75. timers.values.forEach { $0.invalidate() }
  76. timers.removeAll()
  77. }
  78. private func setupPumpConfig() {
  79. // display the pump configuration immediatly
  80. if let pump = provider.deviceManager.pumpManager,
  81. let bluetooth = provider.bluetoothProvider
  82. {
  83. let view = PumpConfig.PumpSettingsView(
  84. pumpManager: pump,
  85. bluetoothManager: bluetooth,
  86. completionDelegate: self
  87. ).asAny()
  88. router.mainSecondaryModalView.send(view)
  89. }
  90. }
  91. private func setupButton(message _: MessageContent, view: MessageView) {
  92. view.button?.setImage(UIImage(), for: .normal)
  93. view.iconLabel = nil
  94. let buttonImage = UIImage(systemName: "chevron.right")?.withTintColor(.white)
  95. view.button?.setImage(buttonImage, for: .normal)
  96. view.button?.backgroundColor = view.backgroundView.backgroundColor
  97. view.button?.tintColor = view.iconImageView?.tintColor // .foregroundColor
  98. }
  99. private func setupAction(message: MessageContent, view: MessageView) {
  100. switch message.action {
  101. case .snooze:
  102. setupButton(message: message, view: view)
  103. view.buttonTapHandler = { _ in
  104. // Popup Snooze view when user taps on Glucose Notification
  105. SwiftMessages.hide()
  106. self.router.mainModalScreen.send(.snooze)
  107. }
  108. case .pumpConfig:
  109. setupButton(message: message, view: view)
  110. view.buttonTapHandler = { _ in
  111. SwiftMessages.hide()
  112. self.setupPumpConfig()
  113. }
  114. default: // break
  115. view.button?.setImage(UIImage(), for: .normal)
  116. view.buttonTapHandler = { _ in
  117. SwiftMessages.hide()
  118. }
  119. }
  120. }
  121. private func isApnPumpConfigAction(_ message: MessageContent) -> Bool {
  122. if message.type != .error, message.action == .pumpConfig {
  123. setupPumpConfig()
  124. return true
  125. }
  126. return false
  127. }
  128. private func allowNotify(_ message: MessageContent) -> Bool {
  129. if message.type == .error { return true } // .errorPump
  130. switch message.subtype {
  131. case .pump:
  132. guard settingsManager.settings.notificationsPump else { return false }
  133. case .cgm:
  134. guard settingsManager.settings.notificationsCgm else { return false }
  135. case .carb:
  136. guard settingsManager.settings.notificationsCarb else { return false }
  137. case .glucose:
  138. guard settingsManager.settings.glucoseNotificationsAlways else { return false }
  139. case .algorithm:
  140. guard settingsManager.settings.notificationsAlgorithm else { return false }
  141. case .misc:
  142. return true
  143. }
  144. return true
  145. }
  146. private func showAlertMessage(_ message: MessageContent) {
  147. if message.useAPN, !alertPermissionsChecker.notificationsDisabled
  148. {
  149. showAPN(message)
  150. } else {
  151. showSwiftMessage(message)
  152. }
  153. }
  154. private func showAPN(_ message: MessageContent) {
  155. DispatchQueue.main.async {
  156. self.broadcaster.notify(alertMessageNotificationObserver.self, on: .main) {
  157. $0.alertMessageNotification(message)
  158. }
  159. }
  160. }
  161. // Read the color scheme preference from UserDefaults; defaults to system default setting
  162. @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
  163. private func showSwiftMessage(_ message: MessageContent) {
  164. // SwiftMessages.pauseBetweenMessages = 1.0
  165. if snoozeUntilDate > Date(), message.action == .snooze {
  166. return
  167. }
  168. var config = SwiftMessages.defaultConfig
  169. let view = MessageView.viewFromNib(layout: .cardView)
  170. view.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
  171. config.prefersStatusBarHidden = true
  172. // Set id so that multiple notifications are not queued while waiting for user response; only the latest will be shown
  173. if message.subtype == .glucose || message.subtype == .carb {
  174. view.id = message.type.rawValue + message.subtype.rawValue
  175. }
  176. let titleContent: String
  177. let iconName = UIApplication.shared.alternateIconName ?? "trioBlack"
  178. let iconImage = UIImage(named: iconName) ?? UIImage()
  179. view.configureContent(
  180. title: "title",
  181. body: NSLocalizedString(message.content, comment: "Info message"),
  182. iconImage: nil,
  183. iconText: nil,
  184. buttonImage: nil,
  185. buttonTitle: nil,
  186. buttonTapHandler: nil
  187. )
  188. view.configureIcon(withSize: CGSize(width: 40, height: 40), contentMode: .scaleAspectFit)
  189. view.iconImageView!.image = iconImage
  190. view.iconImageView?.layer.cornerRadius = 10
  191. view.customConfigureTheme(
  192. colorSchemePreference: colorSchemePreference
  193. )
  194. view.iconImageView?.image = iconImage
  195. switch message.type {
  196. case .info,
  197. .other:
  198. config.duration = .seconds(seconds: 5)
  199. titleContent = message.title != "" ? message.title : NSLocalizedString("Info", comment: "Info title")
  200. case .warning:
  201. config.duration = .forever
  202. titleContent = message.title != "" ? message
  203. .title : NSLocalizedString("Warning", comment: "Warning title")
  204. case .error:
  205. config.duration = .forever
  206. titleContent = message.title != "" ? message
  207. .title : NSLocalizedString("Error", comment: "Error title")
  208. }
  209. view.titleLabel?.text = titleContent
  210. config.dimMode = .gray(interactive: true)
  211. setupAction(message: message, view: view)
  212. if message.trigger != nil {
  213. addOrReplaceTriggerTimer(message: message, config: config, view: view)
  214. }
  215. guard message.type == .error || message.action != .pumpConfig, message.trigger == nil, !view.isHidden else { return }
  216. SwiftMessages.show(config: config, view: view)
  217. }
  218. override func subscribe() {
  219. router.mainModalScreen
  220. .map { $0?.modal(resolver: self.resolver!) }
  221. .removeDuplicates { $0?.id == $1?.id }
  222. .receive(on: DispatchQueue.main)
  223. .sink { modal in
  224. self.modal = modal
  225. self.isModalPresented = modal != nil
  226. }
  227. .store(in: &lifetime)
  228. $isModalPresented
  229. .filter { !$0 }
  230. .sink { _ in
  231. self.router.mainModalScreen.send(nil)
  232. }
  233. .store(in: &lifetime)
  234. router.alertMessage
  235. .receive(on: DispatchQueue.main)
  236. .sink { message in
  237. guard !self.isApnPumpConfigAction(message) else { return }
  238. guard self.allowNotify(message) else { return }
  239. self.showAlertMessage(message)
  240. }
  241. .store(in: &lifetime)
  242. router.mainSecondaryModalView
  243. .receive(on: DispatchQueue.main)
  244. .sink { view in
  245. self.secondaryModalView = view
  246. self.isSecondaryModalPresented = view != nil
  247. }
  248. .store(in: &lifetime)
  249. $isSecondaryModalPresented
  250. .removeDuplicates()
  251. .filter { !$0 }
  252. .sink { _ in
  253. self.router.mainSecondaryModalView.send(nil)
  254. }
  255. .store(in: &lifetime)
  256. }
  257. }
  258. }
  259. extension MessageView {
  260. func currentColorScheme() -> ColorScheme {
  261. let userInterfaceStyle = UITraitCollection.current.userInterfaceStyle
  262. return userInterfaceStyle == .dark ? .dark : .light
  263. }
  264. func customConfigureTheme(colorSchemePreference: ColorSchemeOption) {
  265. let defaultSystemColorScheme = currentColorScheme()
  266. var backgroundColor = UIColor.systemBackground
  267. var foregroundColor = UIColor.white
  268. let ApnBackground = UIColor(named: "ApnBackground") ?? UIColor.lightGray
  269. let iOSlightTrioDark = UIColor(named: "ApnBackgroundLightDark") ?? UIColor.lightGray
  270. switch colorSchemePreference {
  271. case .systemDefault:
  272. backgroundColor = ApnBackground
  273. foregroundColor = UIColor.label
  274. case .dark:
  275. backgroundColor = defaultSystemColorScheme == .light ? iOSlightTrioDark : ApnBackground
  276. foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
  277. case .light:
  278. backgroundColor = defaultSystemColorScheme == .light ? ApnBackground : UIColor.gray
  279. foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
  280. }
  281. iconImageView?.tintColor = foregroundColor
  282. backgroundView.backgroundColor = backgroundColor
  283. titleLabel?.textColor = foregroundColor
  284. bodyLabel?.textColor = foregroundColor
  285. iconImageView?.isHidden = iconImageView?.image == nil
  286. backgroundView.layer.cornerRadius = 25
  287. let adjustedFont = UIFont.systemFont(ofSize: 13.0, weight: .bold)
  288. let preferredTitleFont = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: adjustedFont)
  289. let preferredBodyFont = UIFont.preferredFontforStyle(forTextStyle: .footnote)
  290. // Set the title and body font to the dynamic type sizes
  291. titleLabel?.adjustsFontForContentSizeCategory = true
  292. titleLabel?.font = preferredTitleFont
  293. bodyLabel?.adjustsFontForContentSizeCategory = true
  294. bodyLabel?.font = preferredBodyFont
  295. // Set custom colors for title and body text
  296. titleLabel?.textColor = foregroundColor
  297. bodyLabel?.textColor = foregroundColor
  298. }
  299. }
  300. @available(iOS 16.0, *)
  301. extension Main.StateModel: CompletionDelegate {
  302. func completionNotifyingDidComplete(_: CompletionNotifying) {
  303. // close the window
  304. router.mainSecondaryModalView.send(nil)
  305. }
  306. }
  307. // Extension to convert SwiftUI TextStyle to UIFont
  308. extension UIFont {
  309. static func preferredFontforStyle(forTextStyle: UIFont.TextStyle) -> UIFont {
  310. let uiFontMetrics = UIFontMetrics.default
  311. let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: forTextStyle)
  312. return uiFontMetrics.scaledFont(for: UIFont(descriptor: descriptor, size: 0)) // size: 12
  313. }
  314. }