damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }