NotificationsModel.swift (11499B)
1 // 2 // NotificationsModel.swift 3 // damus 4 // 5 // Created by William Casarin on 2023-02-21. 6 // 7 8 import Foundation 9 10 enum NotificationItem { 11 case repost(NoteId, EventGroup) 12 case reaction(NoteId, EventGroup) 13 case profile_zap(ZapGroup) 14 case event_zap(NoteId, ZapGroup) 15 case reply(NostrEvent) 16 case damus_app_notification(DamusAppNotification) 17 18 var is_reply: NostrEvent? { 19 if case .reply(let ev) = self { 20 return ev 21 } 22 return nil 23 } 24 25 var is_zap: ZapGroup? { 26 switch self { 27 case .profile_zap(let zapgrp): 28 return zapgrp 29 case .event_zap(_, let zapgrp): 30 return zapgrp 31 case .reaction: 32 return nil 33 case .reply: 34 return nil 35 case .repost: 36 return nil 37 case .damus_app_notification(_): 38 return nil 39 } 40 } 41 42 var last_event_at: UInt32 { 43 switch self { 44 case .reaction(_, let evgrp): 45 return evgrp.last_event_at 46 case .repost(_, let evgrp): 47 return evgrp.last_event_at 48 case .profile_zap(let zapgrp): 49 return zapgrp.last_event_at 50 case .event_zap(_, let zapgrp): 51 return zapgrp.last_event_at 52 case .reply(let reply): 53 return reply.created_at 54 case .damus_app_notification(let notification): 55 return notification.last_event_at 56 } 57 } 58 59 func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool { 60 switch self { 61 case .repost(_, let evgrp): 62 return evgrp.would_filter(isIncluded) 63 case .reaction(_, let evgrp): 64 return evgrp.would_filter(isIncluded) 65 case .profile_zap(let zapgrp): 66 return zapgrp.would_filter(isIncluded) 67 case .event_zap(_, let zapgrp): 68 return zapgrp.would_filter(isIncluded) 69 case .reply(let ev): 70 return !isIncluded(ev) 71 case .damus_app_notification(_): 72 return true 73 } 74 } 75 76 func filter(_ isIncluded: (NostrEvent) -> Bool) -> NotificationItem? { 77 switch self { 78 case .repost(let evid, let evgrp): 79 return evgrp.filter(isIncluded).map { .repost(evid, $0) } 80 case .reaction(let evid, let evgrp): 81 return evgrp.filter(isIncluded).map { .reaction(evid, $0) } 82 case .profile_zap(let zapgrp): 83 return zapgrp.filter(isIncluded).map { .profile_zap($0) } 84 case .event_zap(let evid, let zapgrp): 85 return zapgrp.filter(isIncluded).map { .event_zap(evid, $0) } 86 case .reply(let ev): 87 if isIncluded(ev) { return .reply(ev) } 88 return nil 89 case .damus_app_notification(_): 90 return self 91 } 92 } 93 } 94 95 class NotificationsModel: ObservableObject, ScrollQueue { 96 var incoming_zaps: [Zapping] = [] 97 var incoming_events: [NostrEvent] = [] 98 var should_queue: Bool = true 99 100 // mappings from events to 101 var zaps: [NoteId: ZapGroup] = [:] 102 var profile_zaps = ZapGroup() 103 var reactions: [NoteId: EventGroup] = [:] 104 var reposts: [NoteId: EventGroup] = [:] 105 var replies: [NostrEvent] = [] 106 var incoming_app_notifications: [DamusAppNotification] = [] 107 var app_notifications: [DamusAppNotification] = [] 108 var has_app_notification = Set<DamusAppNotification.Content>() 109 var has_reply = Set<NoteId>() 110 var has_ev = Set<NoteId>() 111 112 @Published var notifications: [NotificationItem] = [] 113 114 func set_should_queue(_ val: Bool) { 115 self.should_queue = val 116 } 117 118 func uniq_pubkeys() -> [Pubkey] { 119 var pks = Set<Pubkey>() 120 121 for ev in incoming_events { 122 pks.insert(ev.pubkey) 123 } 124 125 for grp in reposts { 126 for ev in grp.value.events { 127 pks.insert(ev.pubkey) 128 } 129 } 130 131 for ev in replies { 132 pks.insert(ev.pubkey) 133 } 134 135 for zap in incoming_zaps { 136 pks.insert(zap.request.ev.pubkey) 137 } 138 139 return Array(pks) 140 } 141 142 func build_notifications() -> [NotificationItem] { 143 var notifs: [NotificationItem] = [] 144 145 for el in zaps { 146 let evid = el.key 147 let zapgrp = el.value 148 149 let notif: NotificationItem = .event_zap(evid, zapgrp) 150 notifs.append(notif) 151 } 152 153 if !profile_zaps.zaps.isEmpty { 154 notifs.append(.profile_zap(profile_zaps)) 155 } 156 157 for el in reposts { 158 let evid = el.key 159 let evgrp = el.value 160 161 notifs.append(.repost(evid, evgrp)) 162 } 163 164 for el in reactions { 165 let evid = el.key 166 let evgrp = el.value 167 168 notifs.append(.reaction(evid, evgrp)) 169 } 170 171 for reply in replies { 172 notifs.append(.reply(reply)) 173 } 174 175 for app_notification in app_notifications { 176 notifs.append(.damus_app_notification(app_notification)) 177 } 178 179 notifs.sort { $0.last_event_at > $1.last_event_at } 180 return notifs 181 } 182 183 184 private func insert_repost(_ ev: NostrEvent, cache: EventCache) -> Bool { 185 guard let reposted_ev = ev.get_inner_event(cache: cache) else { 186 return false 187 } 188 189 let id = reposted_ev.id 190 191 if let evgrp = self.reposts[id] { 192 return evgrp.insert(ev) 193 } else { 194 let evgrp = EventGroup() 195 self.reposts[id] = evgrp 196 return evgrp.insert(ev) 197 } 198 } 199 200 private func insert_text(_ ev: NostrEvent) -> Bool { 201 guard !has_reply.contains(ev.id) else { 202 return false 203 } 204 205 has_reply.insert(ev.id) 206 replies.append(ev) 207 208 return true 209 } 210 211 private func insert_reaction(_ ev: NostrEvent) -> Bool { 212 guard let id = ev.referenced_ids.last else { 213 return false 214 } 215 216 if let evgrp = self.reactions[id] { 217 return evgrp.insert(ev) 218 } else { 219 let evgrp = EventGroup() 220 self.reactions[id] = evgrp 221 return evgrp.insert(ev) 222 } 223 } 224 225 private func insert_event_immediate(_ ev: NostrEvent, cache: EventCache) -> Bool { 226 if ev.known_kind == .boost { 227 return insert_repost(ev, cache: cache) 228 } else if ev.known_kind == .like { 229 return insert_reaction(ev) 230 } else if ev.known_kind == .text { 231 return insert_text(ev) 232 } 233 234 return false 235 } 236 237 private func insert_zap_immediate(_ zap: Zapping) -> Bool { 238 switch zap.target { 239 case .note(let notezt): 240 let id = notezt.note_id 241 if let zapgrp = self.zaps[notezt.note_id] { 242 return zapgrp.insert(zap) 243 } else { 244 let zapgrp = ZapGroup() 245 self.zaps[id] = zapgrp 246 return zapgrp.insert(zap) 247 } 248 249 case .profile: 250 return profile_zaps.insert(zap) 251 } 252 } 253 254 func insert_event(_ ev: NostrEvent, damus_state: DamusState) -> Bool { 255 if has_ev.contains(ev.id) { 256 return false 257 } 258 259 if should_queue { 260 incoming_events.append(ev) 261 has_ev.insert(ev.id) 262 return true 263 } 264 265 if insert_event_immediate(ev, cache: damus_state.events) { 266 self.notifications = build_notifications() 267 return true 268 } 269 270 return false 271 } 272 273 func insert_app_notification(notification: DamusAppNotification) -> Bool { 274 if has_app_notification.contains(notification.content) { 275 return false 276 } 277 278 if should_queue { 279 incoming_app_notifications.append(notification) 280 return true 281 } 282 283 if insert_app_notification_immediate(notification: notification) { 284 self.notifications = build_notifications() 285 return true 286 } 287 288 return false 289 } 290 291 func insert_app_notification_immediate(notification: DamusAppNotification) -> Bool { 292 if has_app_notification.contains(notification.content) { 293 return false 294 } 295 self.app_notifications.append(notification) 296 has_app_notification.insert(notification.content) 297 return true 298 } 299 300 func insert_zap(_ zap: Zapping) -> Bool { 301 if should_queue { 302 return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap) 303 } 304 305 if insert_zap_immediate(zap) { 306 self.notifications = build_notifications() 307 return true 308 } 309 310 return false 311 } 312 313 func filter(_ isIncluded: (NostrEvent) -> Bool) { 314 var changed = false 315 var count = 0 316 317 count = incoming_events.count 318 incoming_events = incoming_events.filter(isIncluded) 319 changed = changed || incoming_events.count != count 320 321 count = profile_zaps.zaps.count 322 profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) } 323 changed = changed || profile_zaps.zaps.count != count 324 325 for el in reactions { 326 count = el.value.events.count 327 el.value.events = el.value.events.filter(isIncluded) 328 changed = changed || el.value.events.count != count 329 } 330 331 for el in reposts { 332 count = el.value.events.count 333 el.value.events = el.value.events.filter(isIncluded) 334 changed = changed || el.value.events.count != count 335 } 336 337 for el in zaps { 338 count = el.value.zaps.count 339 el.value.zaps = el.value.zaps.filter { 340 isIncluded($0.request.ev) 341 } 342 changed = changed || el.value.zaps.count != count 343 } 344 345 count = replies.count 346 replies = replies.filter(isIncluded) 347 changed = changed || replies.count != count 348 349 if changed { 350 self.notifications = build_notifications() 351 } 352 } 353 354 func flush(_ damus_state: DamusState) -> Bool { 355 var inserted = false 356 357 for zap in incoming_zaps { 358 inserted = insert_zap_immediate(zap) || inserted 359 } 360 361 for event in incoming_events { 362 inserted = insert_event_immediate(event, cache: damus_state.events) || inserted 363 } 364 365 for incoming_app_notification in incoming_app_notifications { 366 inserted = insert_app_notification_immediate(notification: incoming_app_notification) || inserted 367 } 368 369 if inserted { 370 self.notifications = build_notifications() 371 } 372 373 return inserted 374 } 375 } 376 377 struct DamusAppNotification { 378 let notification_timestamp: Date 379 var last_event_at: UInt32 { UInt32(notification_timestamp.timeIntervalSince1970) } 380 let content: Content 381 382 init(content: Content, timestamp: Date) { 383 self.notification_timestamp = timestamp 384 self.content = content 385 } 386 387 enum Content: Hashable, Equatable { 388 case purple_impending_expiration(days_remaining: Int, expiry_date: UInt64) 389 case purple_expired(expiry_date: UInt64) 390 } 391 }