MainStateModel.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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. private var storedMessages: [MessageContent] = []
  15. private let maxStoredMessages = 3
  16. private let maxNotificationsPerMinute = 3
  17. private var lastMessageTimestamp: Date?
  18. private var timer: AnyCancellable?
  19. private var timeInterval: TimeInterval = 1
  20. private let limitInterval: TimeInterval = 20
  21. private var lastNotificationTime: TimeInterval = 0
  22. private var sentNotifications: [TimeInterval] = []
  23. // Method to queue new message and check if it matches the "NOTE-*" pattern
  24. func queueMessageIfNeeded(_ message: MessageContent) {
  25. if message.type != MessageType.info {
  26. showAlertMessage(message)
  27. return
  28. }
  29. if !storedMessages.filter({ $0.content == message.content && $0.title == message.title }).isEmpty { return }
  30. storedMessages.append(message)
  31. lastMessageTimestamp = Date()
  32. // If we have accumulated messages, concatenate and display
  33. if storedMessages.count >= maxStoredMessages {
  34. checkAndDisplayStoredMessages()
  35. } else {
  36. startTimer()
  37. }
  38. }
  39. // Start or restart the timer that checks for the 1-minute interval
  40. private func startTimer() {
  41. timer = Timer.publish(every: timeInterval, on: .main, in: .common)
  42. .autoconnect()
  43. .sink { [weak self] _ in
  44. self?.checkAndDisplayStoredMessages()
  45. }
  46. }
  47. // Method to check the stored messages and show them after 1 minute
  48. private func checkAndDisplayStoredMessages() {
  49. guard !storedMessages.isEmpty else { return }
  50. // Ensure rate limit is not exceeded
  51. let currentTime = Date().timeIntervalSince1970
  52. pruneOldNotifications(currentTime: currentTime)
  53. // Ensure we do not exceed maxNotificationsPerMinute
  54. if sentNotifications.count < maxNotificationsPerMinute {
  55. // If below the limit, send the next notification in the queue
  56. if !alertPermissionsChecker.notificationsDisabled {
  57. let request = storedMessages.removeFirst()
  58. showAlertMessage(request)
  59. sentNotifications.append(currentTime)
  60. } else {
  61. let max = storedMessages.count >= maxStoredMessages ? maxStoredMessages : storedMessages.count
  62. var content = ""
  63. for _ in 1 ... max {
  64. let request = storedMessages.removeFirst()
  65. sentNotifications.append(currentTime)
  66. content = content + request.content + "\n"
  67. }
  68. if content != "" {
  69. let messageCont = MessageContent(
  70. content: content,
  71. type: MessageType.other
  72. )
  73. showAlertMessage(messageCont)
  74. }
  75. }
  76. }
  77. }
  78. // Remove notifications from the sent list that are older than `limitInterval`
  79. private func pruneOldNotifications(currentTime: TimeInterval) {
  80. // Remove any notifications older than `limitInterval`
  81. sentNotifications = sentNotifications.filter { currentTime - $0 < limitInterval }
  82. }
  83. private func showAlertMessage(_ message: MessageContent) {
  84. if message.useAPN, !alertPermissionsChecker.notificationsDisabled, message.type != MessageType.pumpConfig {
  85. showAPN(message)
  86. } else {
  87. showSwiftMessage(message)
  88. }
  89. }
  90. private func showAPN(_ message: MessageContent) {
  91. let messageCont = MessageContent(content: message.content, type: message.type)
  92. switch message.type {
  93. case .pumpConfig:
  94. if let pump = provider.deviceManager.pumpManager,
  95. let bluetooth = provider.bluetoothProvider
  96. {
  97. let view = PumpConfig.PumpSettingsView(
  98. pumpManager: pump,
  99. bluetoothManager: bluetooth,
  100. completionDelegate: self
  101. ).asAny()
  102. router.mainSecondaryModalView.send(view)
  103. }
  104. default:
  105. DispatchQueue.main.async {
  106. self.broadcaster.notify(alertMessageNotificationObserver.self, on: .main) {
  107. $0.alertMessageNotification(messageCont)
  108. }
  109. }
  110. }
  111. }
  112. private func showSwiftMessage(_ message: MessageContent) {
  113. // SwiftMessages.pauseBetweenMessages = 1.0
  114. var config = SwiftMessages.defaultConfig
  115. let view = MessageView.viewFromNib(layout: .cardView)
  116. let titleContent: String
  117. view.configureContent(
  118. title: "title",
  119. body: NSLocalizedString(message.content, comment: "Info message"),
  120. iconImage: nil,
  121. iconText: nil,
  122. buttonImage: nil,
  123. buttonTitle: nil,
  124. buttonTapHandler: nil
  125. )
  126. switch message.type {
  127. case .info,
  128. .other:
  129. view.backgroundColor = .secondarySystemGroupedBackground
  130. config.duration = .automatic
  131. titleContent = message.title != "" ? message.title : NSLocalizedString("Info", comment: "Info title")
  132. case .warning:
  133. view.configureTheme(.warning, iconStyle: .subtle)
  134. config.duration = .forever
  135. view.button?.setImage(Icon.warningSubtle.image, for: .normal)
  136. titleContent = message.title != "" ? message
  137. .title : NSLocalizedString("Warning", comment: "Warning title")
  138. view.buttonTapHandler = { _ in
  139. SwiftMessages.hide()
  140. }
  141. case .errorPump:
  142. view.configureTheme(.error, iconStyle: .subtle)
  143. config.duration = .forever
  144. view.button?.setImage(Icon.errorSubtle.image, for: .normal)
  145. titleContent = message.title != "" ? message
  146. .title : NSLocalizedString("Error", comment: "Error title")
  147. view.buttonTapHandler = { _ in
  148. SwiftMessages.hide()
  149. // display the pump configuration immediatly
  150. if let pump = self.provider.deviceManager.pumpManager,
  151. let bluetooth = self.provider.bluetoothProvider
  152. {
  153. let view = PumpConfig.PumpSettingsView(
  154. pumpManager: pump,
  155. bluetoothManager: bluetooth,
  156. completionDelegate: self
  157. ).asAny()
  158. self.router.mainSecondaryModalView.send(view)
  159. }
  160. }
  161. case .alertPermissionWarning:
  162. view.configureTheme(.error, iconStyle: .none)
  163. config.duration = .forever
  164. view.iconLabel = nil
  165. view.iconImageView = nil
  166. let disclosureIndicator = UIImage(systemName: "chevron.right")?.withTintColor(.white)
  167. view.button?.setImage(disclosureIndicator, for: .normal)
  168. view.button?.backgroundColor = UIColor.red
  169. view.button?.tintColor = UIColor.white
  170. titleContent = message.title != "" ? message
  171. .title : NSLocalizedString("Error", comment: "Error title")
  172. view.buttonTapHandler = { _ in
  173. SwiftMessages.hide()
  174. UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
  175. }
  176. case .pumpConfig:
  177. titleContent = ""
  178. if let pump = provider.deviceManager.pumpManager,
  179. let bluetooth = provider.bluetoothProvider
  180. {
  181. let view = PumpConfig.PumpSettingsView(
  182. pumpManager: pump,
  183. bluetoothManager: bluetooth,
  184. completionDelegate: self
  185. ).asAny()
  186. router.mainSecondaryModalView.send(view)
  187. }
  188. }
  189. if message.type != .pumpConfig
  190. {
  191. view.titleLabel?.text = titleContent
  192. config.dimMode = .gray(interactive: true)
  193. // Show if not hidden
  194. if !view.isHidden {
  195. SwiftMessages.show(config: config, view: view)
  196. }
  197. }
  198. }
  199. override func subscribe() {
  200. router.mainModalScreen
  201. .map { $0?.modal(resolver: self.resolver!) }
  202. .removeDuplicates { $0?.id == $1?.id }
  203. .receive(on: DispatchQueue.main)
  204. .sink { modal in
  205. self.modal = modal
  206. self.isModalPresented = modal != nil
  207. }
  208. .store(in: &lifetime)
  209. $isModalPresented
  210. .filter { !$0 }
  211. .sink { _ in
  212. self.router.mainModalScreen.send(nil)
  213. }
  214. .store(in: &lifetime)
  215. router.alertMessage
  216. .receive(on: DispatchQueue.main)
  217. .sink { message in
  218. self.queueMessageIfNeeded(message)
  219. }
  220. .store(in: &lifetime)
  221. router.mainSecondaryModalView
  222. .receive(on: DispatchQueue.main)
  223. .sink { view in
  224. self.secondaryModalView = view
  225. self.isSecondaryModalPresented = view != nil
  226. }
  227. .store(in: &lifetime)
  228. $isSecondaryModalPresented
  229. .removeDuplicates()
  230. .filter { !$0 }
  231. .sink { _ in
  232. self.router.mainSecondaryModalView.send(nil)
  233. }
  234. .store(in: &lifetime)
  235. }
  236. }
  237. }
  238. @available(iOS 16.0, *)
  239. extension Main.StateModel: CompletionDelegate {
  240. func completionNotifyingDidComplete(_: CompletionNotifying) {
  241. // close the window
  242. router.mainSecondaryModalView.send(nil)
  243. }
  244. }