damus

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

HomeModel.swift (39118B)


      1 //
      2 //  HomeModel.swift
      3 //  damus
      4 //
      5 //  Created by William Casarin on 2022-05-24.
      6 //
      7 
      8 import Foundation
      9 import UIKit
     10 
     11 enum Resubscribe {
     12     case following
     13     case unfollowing(FollowRef)
     14 }
     15 
     16 enum HomeResubFilter {
     17     case pubkey(Pubkey)
     18     case hashtag(String)
     19 
     20     init?(from: FollowRef) {
     21         switch from {
     22         case .hashtag(let ht): self = .hashtag(ht.string())
     23         case .pubkey(let pk):  self = .pubkey(pk)
     24         }
     25 
     26         return nil
     27     }
     28 
     29     func filter(contacts: Contacts, ev: NostrEvent) -> Bool {
     30         switch self {
     31         case .pubkey(let pk):
     32             return ev.pubkey == pk
     33         case .hashtag(let ht):
     34             if contacts.is_friend(ev.pubkey) {
     35                 return false
     36             }
     37             return ev.referenced_hashtags.contains(where: { ref_ht in
     38                 ht == ref_ht.hashtag
     39             })
     40         }
     41     }
     42 }
     43 
     44 class HomeModel {
     45     // Don't trigger a user notification for events older than a certain age
     46     static let event_max_age_for_notification: TimeInterval = EVENT_MAX_AGE_FOR_NOTIFICATION
     47     
     48     var damus_state: DamusState
     49 
     50     // NDBTODO: let's get rid of this entirely, let nostrdb handle it
     51     var has_event: [String: Set<NoteId>] = [:]
     52     var deleted_events: Set<NoteId> = Set()
     53     var last_event_of_kind: [RelayURL: [UInt32: NostrEvent]] = [:]
     54     var done_init: Bool = false
     55     var incoming_dms: [NostrEvent] = []
     56     let dm_debouncer = Debouncer(interval: 0.5)
     57     let resub_debouncer = Debouncer(interval: 3.0)
     58     var should_debounce_dms = true
     59 
     60     let home_subid = UUID().description
     61     let contacts_subid = UUID().description
     62     let notifications_subid = UUID().description
     63     let dms_subid = UUID().description
     64     let init_subid = UUID().description
     65     let profiles_subid = UUID().description
     66     
     67     var loading: Bool = false
     68 
     69     var signal = SignalModel()
     70     
     71     var notifications = NotificationsModel()
     72     var notification_status = NotificationStatusModel()
     73     var events: EventHolder = EventHolder()
     74     var zap_button: ZapButtonModel = ZapButtonModel()
     75     
     76     init() {
     77         self.damus_state = DamusState.empty
     78         self.setup_debouncer()
     79         filter_events()
     80         events.on_queue = preloader
     81         //self.events = EventHolder(on_queue: preloader)
     82     }
     83     
     84     func preloader(ev: NostrEvent) {
     85         preload_events(state: self.damus_state, events: [ev])
     86     }
     87     
     88     var pool: RelayPool {
     89         return damus_state.pool
     90     }
     91     
     92     var dms: DirectMessagesModel {
     93         return damus_state.dms
     94     }
     95 
     96     func has_sub_id_event(sub_id: String, ev_id: NoteId) -> Bool {
     97         if !has_event.keys.contains(sub_id) {
     98             has_event[sub_id] = Set()
     99             return false
    100         }
    101 
    102         return has_event[sub_id]!.contains(ev_id)
    103     }
    104     
    105     func setup_debouncer() {
    106         // turn off debouncer after initial load
    107         DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
    108             self.should_debounce_dms = false
    109         }
    110     }
    111 
    112     func resubscribe(_ resubbing: Resubscribe) {
    113         if self.should_debounce_dms {
    114             // don't resub on initial load
    115             return
    116         }
    117 
    118         print("hit resub debouncer")
    119 
    120         resub_debouncer.debounce {
    121             print("resub")
    122             self.unsubscribe_to_home_filters()
    123 
    124             switch resubbing {
    125             case .following:
    126                 break
    127             case .unfollowing(let r):
    128                 if let filter = HomeResubFilter(from: r) {
    129                     self.events.filter { ev in !filter.filter(contacts: self.damus_state.contacts, ev: ev) }
    130                 }
    131             }
    132 
    133             self.subscribe_to_home_filters()
    134         }
    135     }
    136 
    137     @MainActor
    138     func process_event(sub_id: String, relay_id: RelayURL, ev: NostrEvent) {
    139         if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
    140             return
    141         }
    142 
    143         let last_k = get_last_event_of_kind(relay_id: relay_id, kind: ev.kind)
    144         if last_k == nil || ev.created_at > last_k!.created_at {
    145             last_event_of_kind[relay_id]?[ev.kind] = ev
    146         }
    147 
    148         guard let kind = ev.known_kind else {
    149             return
    150         }
    151 
    152         switch kind {
    153         case .chat, .longform, .text:
    154             handle_text_event(sub_id: sub_id, ev)
    155         case .contacts:
    156             handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
    157         case .metadata:
    158             // profile metadata processing is handled by nostrdb
    159             break
    160         case .list_deprecated:
    161             handle_old_list_event(ev)
    162         case .mute_list:
    163             handle_mute_list_event(ev)
    164         case .boost:
    165             handle_boost_event(sub_id: sub_id, ev)
    166         case .like:
    167             handle_like_event(ev)
    168         case .dm:
    169             handle_dm(ev)
    170         case .delete:
    171             handle_delete_event(ev)
    172         case .zap:
    173             handle_zap_event(ev)
    174         case .zap_request:
    175             break
    176         case .nwc_request:
    177             break
    178         case .nwc_response:
    179             handle_nwc_response(ev, relay: relay_id)
    180         case .http_auth:
    181             break
    182         case .status:
    183             handle_status_event(ev)
    184         }
    185     }
    186 
    187     @MainActor
    188     func handle_status_event(_ ev: NostrEvent) {
    189         guard let st = UserStatus(ev: ev) else {
    190             return
    191         }
    192 
    193         // don't process expired events
    194         if let expires = st.expires_at, Date.now >= expires {
    195             return
    196         }
    197 
    198         let pdata = damus_state.profiles.profile_data(ev.pubkey)
    199 
    200         // don't use old events
    201         if st.type == .music,
    202            let music = pdata.status.music,
    203            ev.created_at < music.created_at {
    204             return
    205         } else if st.type == .general,
    206                   let general = pdata.status.general,
    207                   ev.created_at < general.created_at {
    208             return
    209         }
    210 
    211         pdata.status.update_status(st)
    212     }
    213 
    214     func handle_nwc_response(_ ev: NostrEvent, relay: RelayURL) {
    215         Task { @MainActor in
    216             // TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
    217             guard let nwc_str = damus_state.settings.nostr_wallet_connect,
    218                   let nwc = WalletConnectURL(str: nwc_str),
    219                   let resp = await FullWalletResponse(from: ev, nwc: nwc) else {
    220                 return
    221             }
    222 
    223             // since command results are not returned for ephemeral events,
    224             // remove the request from the postbox which is likely failing over and over
    225             if damus_state.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) {
    226                 print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]")
    227             } else {
    228                 print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
    229             }
    230             
    231             guard resp.response.error == nil else {
    232                 print("nwc error: \(resp.response)")
    233                 nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
    234                 return
    235             }
    236             
    237             print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
    238             nwc_success(state: self.damus_state, resp: resp)
    239         }
    240     }
    241 
    242     @MainActor
    243     func handle_zap_event(_ ev: NostrEvent) {
    244         process_zap_event(state: damus_state, ev: ev) { zapres in
    245             guard case .done(let zap) = zapres,
    246                   zap.target.pubkey == self.damus_state.keypair.pubkey,
    247                   should_show_event(state: self.damus_state, ev: zap.request.ev) else {
    248                 return
    249             }
    250         
    251             if !self.notifications.insert_zap(.zap(zap)) {
    252                 return
    253             }
    254 
    255             guard let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: .notifications, shouldNotify: true) else {
    256                 return
    257             }
    258             
    259             if self.damus_state.settings.zap_vibration {
    260                 // Generate zap vibration
    261                 zap_vibrate(zap_amount: zap.invoice.amount)
    262             }
    263             
    264             if self.damus_state.settings.zap_notification {
    265                 // Create in-app local notification for zap received.
    266                 switch zap.target {
    267                 case .profile(let profile_id):
    268                     create_in_app_profile_zap_notification(profiles: self.damus_state.profiles, zap: zap, profile_id: profile_id)
    269                 case .note(let note_target):
    270                     create_in_app_event_zap_notification(profiles: self.damus_state.profiles, zap: zap, evId: note_target.note_id)
    271                 }
    272             }
    273             
    274             self.notification_status.new_events = new_bits
    275         }
    276         
    277     }
    278     
    279     @MainActor
    280     func handle_damus_app_notification(_ notification: DamusAppNotification) async {
    281         if self.notifications.insert_app_notification(notification: notification) {
    282             // If we successfully inserted a new Damus App notification, switch ON the Damus App notification bit on our NewsEventsBits
    283             // This will cause the bell icon on the tab bar to display the purple dot indicating there is an unread notification
    284             self.notification_status.new_events = NewEventsBits(rawValue: self.notification_status.new_events.rawValue | NewEventsBits.damus_app_notifications.rawValue)
    285         }
    286     }
    287     
    288     func filter_events() {
    289         events.filter { ev in
    290             !damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil))
    291         }
    292         
    293         self.dms.dms = dms.dms.filter { ev in
    294             !damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil))
    295         }
    296         
    297         notifications.filter { ev in
    298             if damus_state.settings.onlyzaps_mode && ev.known_kind == NostrKind.like {
    299                 return false
    300             }
    301 
    302             let event_muted = damus_state.mutelist_manager.is_event_muted(ev)
    303             return !event_muted
    304         }
    305     }
    306     
    307     func handle_delete_event(_ ev: NostrEvent) {
    308         self.deleted_events.insert(ev.id)
    309     }
    310 
    311     func handle_contact_event(sub_id: String, relay_id: RelayURL, ev: NostrEvent) {
    312         process_contact_event(state: self.damus_state, ev: ev)
    313 
    314         if sub_id == init_subid {
    315             pool.send(.unsubscribe(init_subid), to: [relay_id])
    316             if !done_init {
    317                 done_init = true
    318                 send_home_filters(relay_id: nil)
    319             }
    320         }
    321     }
    322 
    323     func handle_boost_event(sub_id: String, _ ev: NostrEvent) {
    324         var boost_ev_id = ev.last_refid()
    325 
    326         if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
    327             boost_ev_id = inner_ev.id
    328 
    329             Task {
    330                 guard validate_event(ev: inner_ev) == .ok else {
    331                     return
    332                 }
    333                 
    334                 if inner_ev.is_textlike {
    335                     DispatchQueue.main.async {
    336                         self.handle_text_event(sub_id: sub_id, ev)
    337                     }
    338                 }
    339             }
    340         }
    341 
    342         guard let e = boost_ev_id else {
    343             return
    344         }
    345 
    346         switch self.damus_state.boosts.add_event(ev, target: e) {
    347         case .already_counted:
    348             break
    349         case .success(let n):
    350             notify(.update_stats(note_id: e))
    351         }
    352     }
    353 
    354     func handle_quote_repost_event(_ ev: NostrEvent, target: NoteId) {
    355         switch damus_state.quote_reposts.add_event(ev, target: target) {
    356         case .already_counted:
    357             break
    358         case .success(let n):
    359             notify(.update_stats(note_id: target))
    360         }
    361     }
    362 
    363     func handle_like_event(_ ev: NostrEvent) {
    364         guard let e = ev.last_refid() else {
    365             // no id ref? invalid like event
    366             return
    367         }
    368 
    369         if damus_state.settings.onlyzaps_mode {
    370             return
    371         }
    372 
    373         switch damus_state.likes.add_event(ev, target: e) {
    374         case .already_counted:
    375             break
    376         case .success(let n):
    377             handle_notification(ev: ev)
    378             let liked = Counted(event: ev, id: e, total: n)
    379             notify(.liked(liked))
    380             notify(.update_stats(note_id: e))
    381         }
    382     }
    383 
    384     @MainActor
    385     func handle_event(relay_id: RelayURL, conn_event: NostrConnectionEvent) {
    386         switch conn_event {
    387         case .ws_event(let ev):
    388             switch ev {
    389             case .connected:
    390                 if !done_init {
    391                     self.loading = true
    392                     send_initial_filters(relay_id: relay_id)
    393                 } else {
    394                     //remove_bootstrap_nodes(damus_state)
    395                     send_home_filters(relay_id: relay_id)
    396                 }
    397                 
    398                 // connect to nwc relays when connected
    399                 if let nwc_str = damus_state.settings.nostr_wallet_connect,
    400                    let r = pool.get_relay(relay_id),
    401                    r.descriptor.variant == .nwc,
    402                    let nwc = WalletConnectURL(str: nwc_str),
    403                    nwc.relay == relay_id
    404                 {
    405                     subscribe_to_nwc(url: nwc, pool: pool)
    406                 }
    407             case .error(let merr):
    408                 let desc = String(describing: merr)
    409                 if desc.contains("Software caused connection abort") {
    410                     pool.reconnect(to: [relay_id])
    411                 }
    412             case .disconnected:
    413                 pool.reconnect(to: [relay_id])
    414             default:
    415                 break
    416             }
    417             
    418             update_signal_from_pool(signal: self.signal, pool: damus_state.pool)
    419         case .nostr_event(let ev):
    420             switch ev {
    421             case .event(let sub_id, let ev):
    422                 // globally handle likes
    423                 /*
    424                 let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .boost || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
    425                 if !always_process {
    426                     // TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
    427                     return
    428                 }
    429                 */
    430 
    431                 self.process_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
    432             case .notice(let msg):
    433                 print(msg)
    434 
    435             case .eose(let sub_id):
    436                 guard let txn = NdbTxn(ndb: damus_state.ndb) else {
    437                     return
    438                 }
    439 
    440                 if sub_id == dms_subid {
    441                     var dms = dms.dms.flatMap { $0.events }
    442                     dms.append(contentsOf: incoming_dms)
    443                     load_profiles(context: "dms", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state, txn: txn)
    444                 } else if sub_id == notifications_subid {
    445                     load_profiles(context: "notifications", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state, txn: txn)
    446                 } else if sub_id == home_subid {
    447                     load_profiles(context: "home", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus_state, txn: txn)
    448                 }
    449                 
    450                 self.loading = false
    451                 break
    452                 
    453             case .ok:
    454                 break
    455             case .auth:
    456                 break
    457             }
    458             
    459         }
    460     }
    461 
    462 
    463     /// Send the initial filters, just our contact list mostly
    464     func send_initial_filters(relay_id: RelayURL) {
    465         let filter = NostrFilter(kinds: [.contacts], limit: 1, authors: [damus_state.pubkey])
    466         let subscription = NostrSubscribe(filters: [filter], sub_id: init_subid)
    467         pool.send(.subscribe(subscription), to: [relay_id])
    468     }
    469 
    470     /// After initial connection or reconnect, send subscription filters for the home timeline, DMs, and notifications
    471     func send_home_filters(relay_id: RelayURL?) {
    472         // TODO: since times should be based on events from a specific relay
    473         // perhaps we could mark this in the relay pool somehow
    474 
    475         let friends = get_friends()
    476 
    477         var contacts_filter = NostrFilter(kinds: [.metadata])
    478         contacts_filter.authors = friends
    479 
    480         var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
    481         our_contacts_filter.authors = [damus_state.pubkey]
    482         
    483         var our_old_blocklist_filter = NostrFilter(kinds: [.list_deprecated])
    484         our_old_blocklist_filter.parameter = ["mute"]
    485         our_old_blocklist_filter.authors = [damus_state.pubkey]
    486 
    487         var our_blocklist_filter = NostrFilter(kinds: [.mute_list])
    488         our_blocklist_filter.authors = [damus_state.pubkey]
    489 
    490         var dms_filter = NostrFilter(kinds: [.dm])
    491 
    492         var our_dms_filter = NostrFilter(kinds: [.dm])
    493 
    494         // friends only?...
    495         //dms_filter.authors = friends
    496         dms_filter.limit = 500
    497         dms_filter.pubkeys = [ damus_state.pubkey ]
    498         our_dms_filter.authors = [ damus_state.pubkey ]
    499 
    500         var notifications_filter_kinds: [NostrKind] = [
    501             .text,
    502             .boost,
    503             .zap,
    504         ]
    505         if !damus_state.settings.onlyzaps_mode {
    506             notifications_filter_kinds.append(.like)
    507         }
    508         var notifications_filter = NostrFilter(kinds: notifications_filter_kinds)
    509         notifications_filter.pubkeys = [damus_state.pubkey]
    510         notifications_filter.limit = 500
    511 
    512         var notifications_filters = [notifications_filter]
    513         var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter]
    514         var dms_filters = [dms_filter, our_dms_filter]
    515         let last_of_kind = get_last_of_kind(relay_id: relay_id)
    516 
    517         contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters)
    518         notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
    519         dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
    520 
    521         //print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
    522 
    523         subscribe_to_home_filters(relay_id: relay_id)
    524 
    525         let relay_ids = relay_id.map { [$0] }
    526 
    527         pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: relay_ids)
    528         pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: relay_ids)
    529         pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: relay_ids)
    530     }
    531 
    532     func get_last_of_kind(relay_id: RelayURL?) -> [UInt32: NostrEvent] {
    533         return relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
    534     }
    535 
    536     func unsubscribe_to_home_filters() {
    537         pool.send(.unsubscribe(home_subid))
    538     }
    539 
    540     func get_friends() -> [Pubkey] {
    541         var friends = damus_state.contacts.get_friend_list()
    542         friends.insert(damus_state.pubkey)
    543         return Array(friends)
    544     }
    545 
    546     func subscribe_to_home_filters(friends fs: [Pubkey]? = nil, relay_id: RelayURL? = nil) {
    547         // TODO: separate likes?
    548         var home_filter_kinds: [NostrKind] = [
    549             .text, .longform, .boost
    550         ]
    551         if !damus_state.settings.onlyzaps_mode {
    552             home_filter_kinds.append(.like)
    553         }
    554 
    555         // only pull status data if we care for it
    556         if damus_state.settings.show_music_statuses || damus_state.settings.show_general_statuses {
    557             home_filter_kinds.append(.status)
    558         }
    559 
    560         let friends = fs ?? get_friends()
    561         var home_filter = NostrFilter(kinds: home_filter_kinds)
    562         // include our pubkey as well even if we're not technically a friend
    563         home_filter.authors = friends
    564         home_filter.limit = 500
    565 
    566         var home_filters = [home_filter]
    567 
    568         let followed_hashtags = Array(damus_state.contacts.get_followed_hashtags())
    569         if followed_hashtags.count != 0 {
    570             var hashtag_filter = NostrFilter.filter_hashtag(followed_hashtags)
    571             hashtag_filter.limit = 100
    572             home_filters.append(hashtag_filter)
    573         }
    574 
    575         let relay_ids = relay_id.map { [$0] }
    576         home_filters = update_filters_with_since(last_of_kind: get_last_of_kind(relay_id: relay_id), filters: home_filters)
    577         let sub = NostrSubscribe(filters: home_filters, sub_id: home_subid)
    578 
    579         pool.send(.subscribe(sub), to: relay_ids)
    580     }
    581 
    582     func handle_mute_list_event(_ ev: NostrEvent) {
    583         // we only care about our mutelist
    584         guard ev.pubkey == damus_state.pubkey else {
    585             return
    586         }
    587 
    588         // we only care about the most recent mutelist
    589         if let mutelist = damus_state.mutelist_manager.event {
    590             if ev.created_at <= mutelist.created_at {
    591                 return
    592             }
    593         }
    594 
    595         damus_state.mutelist_manager.set_mutelist(ev)
    596 
    597         migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state)
    598     }
    599 
    600     func handle_old_list_event(_ ev: NostrEvent) {
    601         // we only care about our lists
    602         guard ev.pubkey == damus_state.pubkey else {
    603             return
    604         }
    605         
    606         // we only care about the most recent mutelist
    607         if let mutelist = damus_state.mutelist_manager.event {
    608             if ev.created_at <= mutelist.created_at {
    609                 return
    610             }
    611         }
    612         
    613         guard ev.referenced_params.contains(where: { p in p.param.matches_str("mute") }) else {
    614             return
    615         }
    616 
    617         damus_state.mutelist_manager.set_mutelist(ev)
    618 
    619         migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state)
    620     }
    621 
    622     func get_last_event_of_kind(relay_id: RelayURL, kind: UInt32) -> NostrEvent? {
    623         guard let m = last_event_of_kind[relay_id] else {
    624             last_event_of_kind[relay_id] = [:]
    625             return nil
    626         }
    627 
    628         return m[kind]
    629     }
    630     
    631     func handle_notification(ev: NostrEvent) {
    632         // don't show notifications from ourselves
    633         guard ev.pubkey != damus_state.pubkey,
    634               event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey),
    635               should_show_event(state: damus_state, ev: ev) else {
    636             return
    637         }
    638         
    639         damus_state.events.insert(ev)
    640         
    641         if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
    642             damus_state.events.insert(inner_ev)
    643         }
    644         
    645         if !notifications.insert_event(ev, damus_state: damus_state) {
    646             return
    647         }
    648         
    649         if handle_last_event(ev: ev, timeline: .notifications) {
    650             process_local_notification(state: damus_state, event: ev)
    651         }
    652         
    653     }
    654 
    655     @discardableResult
    656     func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool {
    657         if let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
    658             self.notification_status.new_events = new_bits
    659             return true
    660         } else {
    661             return false
    662         }
    663     }
    664 
    665     func insert_home_event(_ ev: NostrEvent) {
    666         if events.insert(ev) {
    667             handle_last_event(ev: ev, timeline: .home)
    668         }
    669     }
    670 
    671 
    672     func handle_text_event(sub_id: String, _ ev: NostrEvent) {
    673         guard should_show_event(state: damus_state, ev: ev) else {
    674             return
    675         }
    676         
    677         // TODO: will we need to process this in other places like zap request contents, etc?
    678         process_image_metadatas(cache: damus_state.events, ev: ev)
    679         damus_state.replies.count_replies(ev, keypair: self.damus_state.keypair)
    680         damus_state.events.insert(ev)
    681 
    682         if let quoted_event = ev.referenced_quote_ids.first {
    683             handle_quote_repost_event(ev, target: quoted_event.note_id)
    684         }
    685 
    686         if sub_id == home_subid {
    687             insert_home_event(ev)
    688         } else if sub_id == notifications_subid {
    689             handle_notification(ev: ev)
    690         }
    691     }
    692     
    693     func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
    694         notification_status.new_events = notifs
    695         
    696         guard should_display_notification(state: damus_state, event: ev),
    697               let notification_object = generate_local_notification_object(from: ev, state: damus_state)
    698         else {
    699             return
    700         }
    701         
    702         create_local_notification(profiles: damus_state.profiles, notify: notification_object)
    703     }
    704     
    705     func handle_dm(_ ev: NostrEvent) {
    706         guard should_show_event(state: damus_state, ev: ev) else {
    707             return
    708         }
    709         
    710         damus_state.events.insert(ev)
    711         
    712         if !should_debounce_dms {
    713             self.incoming_dms.append(ev)
    714             if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
    715                 got_new_dm(notifs: notifs, ev: ev)
    716             }
    717             self.incoming_dms = []
    718             return
    719         }
    720         
    721         incoming_dms.append(ev)
    722         
    723         dm_debouncer.debounce { [self] in
    724             if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
    725                 got_new_dm(notifs: notifs, ev: ev)
    726             }
    727             self.incoming_dms = []
    728         }
    729     }
    730 }
    731 
    732 
    733 func update_signal_from_pool(signal: SignalModel, pool: RelayPool) {
    734     if signal.max_signal != pool.relays.count {
    735         signal.max_signal = pool.relays.count
    736     }
    737 
    738     if signal.signal != pool.num_connected {
    739         signal.signal = pool.num_connected
    740     }
    741 }
    742 
    743 func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
    744     if !contacts.is_friend(ev.pubkey) {
    745         return
    746     }
    747 
    748     contacts.add_friend_contact(ev)
    749 }
    750 
    751 func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
    752     let contacts = state.contacts
    753     let new_refs = Set<FollowRef>(ev.referenced_follows)
    754     let old_refs = m_old_ev.map({ old_ev in Set(old_ev.referenced_follows) }) ?? Set()
    755 
    756     let diff = new_refs.symmetricDifference(old_refs)
    757     for ref in diff {
    758         if new_refs.contains(ref) {
    759             notify(.followed(ref))
    760             switch ref {
    761             case .pubkey(let pk):
    762                 contacts.add_friend_pubkey(pk)
    763             case .hashtag:
    764                 // I guess I could cache followed hashtags here... whatever
    765                 break
    766             }
    767         } else {
    768             notify(.unfollowed(ref))
    769             switch ref {
    770             case .pubkey(let pk):
    771                 contacts.remove_friend(pk)
    772             case .hashtag: break
    773             }
    774         }
    775     }
    776 }
    777 
    778 
    779 func abbrev_ids(_ ids: [String]) -> String {
    780     if ids.count > 5 {
    781         let n = ids.count - 5
    782         return "[" + ids[..<5].joined(separator: ",") + ", ... (\(n) more)]"
    783     }
    784     return "\(ids)"
    785 }
    786 
    787 func abbrev_field<T: CustomStringConvertible>(_ n: String, _ field: T?) -> String {
    788     guard let field = field else {
    789         return ""
    790     }
    791 
    792     return "\(n):\(field.description)"
    793 }
    794 
    795 func abbrev_ids_field(_ n: String, _ ids: [String]?) -> String {
    796     guard let ids = ids else {
    797         return ""
    798     }
    799 
    800     return "\(n): \(abbrev_ids(ids))"
    801 }
    802 
    803 /*
    804 func print_filter(_ f: NostrFilter) {
    805     let fmt = [
    806         abbrev_ids_field("ids", f.ids),
    807         abbrev_field("kinds", f.kinds),
    808         abbrev_ids_field("authors", f.authors),
    809         abbrev_ids_field("referenced_ids", f.referenced_ids),
    810         abbrev_ids_field("pubkeys", f.pubkeys),
    811         abbrev_field("since", f.since),
    812         abbrev_field("until", f.until),
    813         abbrev_field("limit", f.limit)
    814     ].filter({ !$0.isEmpty }).joined(separator: ",")
    815 
    816     print("Filter(\(fmt))")
    817 }
    818 
    819 func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
    820     let relays = relay_id ?? "relays"
    821     print("connected to \(relays) with filters:")
    822     for group in groups {
    823         for filter in group {
    824             print_filter(filter)
    825         }
    826     }
    827     print("-----")
    828 }
    829  */
    830 
    831 // TODO: remove this, let nostrdb handle all validation
    832 func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
    833     let validated = events.is_event_valid(ev.id)
    834     
    835     switch validated {
    836     case .unknown:
    837         Task.detached(priority: .medium) {
    838             let result = validate_event(ev: ev)
    839             
    840             DispatchQueue.main.async {
    841                 events.store_event_validation(evid: ev.id, validated: result)
    842                 guard result == .ok else {
    843                     return
    844                 }
    845                 callback()
    846             }
    847         }
    848         
    849     case .ok:
    850         callback()
    851         
    852     case .bad_id, .bad_sig:
    853         break
    854     }
    855 }
    856 
    857 func robohash(_ pk: Pubkey) -> String {
    858     return "https://robohash.org/" + pk.hex()
    859 }
    860 
    861 func load_our_stuff(state: DamusState, ev: NostrEvent) {
    862     guard ev.pubkey == state.pubkey else {
    863         return
    864     }
    865     
    866     // only use new stuff
    867     if let current_ev = state.contacts.event {
    868         guard ev.created_at > current_ev.created_at else {
    869             return
    870         }
    871     }
    872     
    873     let m_old_ev = state.contacts.event
    874     state.contacts.event = ev
    875 
    876     load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev)
    877     load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
    878 }
    879 
    880 func process_contact_event(state: DamusState, ev: NostrEvent) {
    881     load_our_stuff(state: state, ev: ev)
    882     add_contact_if_friend(contacts: state.contacts, ev: ev)
    883 }
    884 
    885 func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
    886     let bootstrap_dict: [RelayURL: RelayInfo] = [:]
    887     let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? state.bootstrap_relays.reduce(into: bootstrap_dict) { (d, r) in
    888         d[r] = .rw
    889     }
    890 
    891     guard let decoded: [RelayURL: RelayInfo] = decode_json_relays(ev.content) else {
    892         return
    893     }
    894 
    895     var changed = false
    896 
    897     var new = Set<RelayURL>()
    898     for key in decoded.keys {
    899         new.insert(key)
    900     }
    901 
    902     var old = Set<RelayURL>()
    903     for key in old_decoded.keys {
    904         old.insert(key)
    905     }
    906     
    907     let diff = old.symmetricDifference(new)
    908     
    909     let new_relay_filters = load_relay_filters(state.pubkey) == nil
    910     for d in diff {
    911         changed = true
    912         if new.contains(d) {
    913             let descriptor = RelayDescriptor(url: d, info: decoded[d] ?? .rw)
    914             add_new_relay(model_cache: state.relay_model_cache, relay_filters: state.relay_filters, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: state.settings.developer_mode)
    915         } else {
    916             state.pool.remove_relay(d)
    917         }
    918     }
    919     
    920     if changed {
    921         save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
    922         state.pool.connect()
    923         notify(.relays_changed)
    924     }
    925 }
    926 
    927 func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) {
    928     try? pool.add_relay(descriptor)
    929     let url = descriptor.url
    930 
    931     let relay_id = url
    932     guard model_cache.model(withURL: url) == nil else {
    933         return
    934     }
    935     
    936     Task.detached(priority: .background) {
    937         guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
    938             return
    939         }
    940         
    941         await MainActor.run {
    942             let model = RelayModel(url, metadata: meta)
    943             model_cache.insert(model: model)
    944             
    945             if logging_enabled {
    946                 pool.setLog(model.log, for: relay_id)
    947             }
    948             
    949             // if this is the first time adding filters, we should filter non-paid relays
    950             if new_relay_filters && !meta.is_paid {
    951                 relay_filters.insert(timeline: .search, relay_id: relay_id)
    952             }
    953         }
    954     }
    955 }
    956 
    957 func fetch_relay_metadata(relay_id: RelayURL) async throws -> RelayMetadata? {
    958     var urlString = relay_id.absoluteString.replacingOccurrences(of: "wss://", with: "https://")
    959     urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
    960 
    961     guard let url = URL(string: urlString) else {
    962         return nil
    963     }
    964     
    965     var request = URLRequest(url: url)
    966     request.setValue("application/nostr+json", forHTTPHeaderField: "Accept")
    967     
    968     var res: (Data, URLResponse)? = nil
    969     
    970     res = try await URLSession.shared.data(for: request)
    971     
    972     guard let data = res?.0 else {
    973         return nil
    974     }
    975     
    976     let nip11 = try JSONDecoder().decode(RelayMetadata.self, from: data)
    977     return nip11
    978 }
    979 
    980 @discardableResult
    981 func handle_incoming_dm(ev: NostrEvent, our_pubkey: Pubkey, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
    982     var inserted = false
    983     var found = false
    984     
    985     let ours = ev.pubkey == our_pubkey
    986     var i = 0
    987 
    988     var the_pk = ev.pubkey
    989     if ours {
    990         if let ref_pk = ev.referenced_pubkeys.first {
    991             the_pk = ref_pk
    992         } else {
    993             // self dm!?
    994             print("TODO: handle self dm?")
    995         }
    996     }
    997 
    998     for model in dms.dms {
    999         if model.pubkey == the_pk {
   1000             found = true
   1001             inserted = insert_uniq_sorted_event(events: &(dms.dms[i].events), new_ev: ev) {
   1002                 $0.created_at < $1.created_at
   1003             }
   1004 
   1005             break
   1006         }
   1007         i += 1
   1008     }
   1009 
   1010     if !found {
   1011         let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey, pubkey: the_pk)
   1012         dms.dms.append(model)
   1013         inserted = true
   1014     }
   1015     
   1016     var new_bits: NewEventsBits? = nil
   1017     if inserted {
   1018         new_bits = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours)
   1019     }
   1020     
   1021     return (inserted, new_bits)
   1022 }
   1023 
   1024 @discardableResult
   1025 func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: Pubkey, evs: [NostrEvent]) -> NewEventsBits? {
   1026     var inserted = false
   1027 
   1028     var new_events: NewEventsBits? = nil
   1029     
   1030     for ev in evs {
   1031         let res = handle_incoming_dm(ev: ev, our_pubkey: our_pubkey, dms: dms, prev_events: prev_events)
   1032         inserted = res.0 || inserted
   1033         if let new = res.1 {
   1034             new_events = new
   1035         }
   1036     }
   1037     
   1038     if inserted {
   1039         let new_dms = Array(dms.dms.filter({ $0.events.count > 0 })).sorted { a, b in
   1040             return a.events.last!.created_at > b.events.last!.created_at
   1041         }
   1042         
   1043         dms.dms = new_dms
   1044     }
   1045     
   1046     return new_events
   1047 }
   1048 
   1049 func determine_event_notifications(_ ev: NostrEvent) -> NewEventsBits {
   1050     guard let kind = ev.known_kind else {
   1051         return []
   1052     }
   1053     
   1054     if kind == .zap {
   1055         return [.zaps]
   1056     }
   1057     
   1058     if kind == .boost {
   1059         return [.reposts]
   1060     }
   1061     
   1062     if kind == .text {
   1063         return [.mentions]
   1064     }
   1065     
   1066     if kind == .like {
   1067         return [.likes]
   1068     }
   1069     
   1070     return []
   1071 }
   1072 
   1073 func timeline_to_notification_bits(_ timeline: Timeline, ev: NostrEvent?) -> NewEventsBits {
   1074     switch timeline {
   1075     case .home:
   1076         return [.home]
   1077     case .notifications:
   1078         if let ev {
   1079             return determine_event_notifications(ev)
   1080         }
   1081         return [.notifications]
   1082     case .search:
   1083         return [.search]
   1084     case .dms:
   1085         return [.dms]
   1086     }
   1087 }
   1088 
   1089 /// A helper to determine if we need to notify the user of new events
   1090 func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> NewEventsBits? {
   1091     let last_ev = get_last_event(timeline)
   1092 
   1093     if last_ev == nil || last_ev!.created_at < ev.created_at {
   1094         save_last_event(ev, timeline: timeline)
   1095         if shouldNotify {
   1096             return new_events.union(timeline_to_notification_bits(timeline, ev: ev))
   1097         }
   1098     }
   1099     
   1100     return nil
   1101 }
   1102 
   1103 
   1104 /// Sometimes we get garbage in our notifications. Ensure we have our pubkey on this event
   1105 func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: Pubkey) -> Bool {
   1106     return ev.referenced_pubkeys.contains(our_pubkey)
   1107 }
   1108 
   1109 func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool {
   1110     return should_show_event(
   1111         state: damus_state,
   1112         ev: event
   1113     )
   1114 }
   1115 
   1116 func should_show_event(state: DamusState, ev: NostrEvent, keypair: Keypair? = nil) -> Bool {
   1117     let event_muted = state.mutelist_manager.is_event_muted(ev, keypair: keypair)
   1118     if event_muted {
   1119         return false
   1120     }
   1121 
   1122     return ev.should_show_event
   1123 }
   1124 
   1125 func zap_vibrate(zap_amount: Int64) {
   1126     let sats = zap_amount / 1000
   1127     var vibration_generator: UIImpactFeedbackGenerator
   1128     if sats >= 10000 {
   1129         vibration_generator = UIImpactFeedbackGenerator(style: .heavy)
   1130     } else if sats >= 1000 {
   1131         vibration_generator = UIImpactFeedbackGenerator(style: .medium)
   1132     } else {
   1133         vibration_generator = UIImpactFeedbackGenerator(style: .light)
   1134     }
   1135     vibration_generator.impactOccurred()
   1136 }
   1137 
   1138 func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, profile_id: Pubkey) {
   1139     let content = UNMutableNotificationContent()
   1140 
   1141     content.title = NotificationFormatter.zap_notification_title(zap)
   1142     content.body = NotificationFormatter.zap_notification_body(profiles: profiles, zap: zap, locale: locale)
   1143     content.sound = UNNotificationSound.default
   1144     content.userInfo = LossyLocalNotification(type: .profile_zap, mention: .pubkey(profile_id)).to_user_info()
   1145 
   1146     let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
   1147 
   1148     let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
   1149 
   1150     UNUserNotificationCenter.current().add(request) { error in
   1151         if let error = error {
   1152             print("Error: \(error)")
   1153         } else {
   1154             print("Local notification scheduled")
   1155         }
   1156     }
   1157 }
   1158 
   1159 func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: NoteId) {
   1160     let content = UNMutableNotificationContent()
   1161 
   1162     content.title = NotificationFormatter.zap_notification_title(zap)
   1163     content.body = NotificationFormatter.zap_notification_body(profiles: profiles, zap: zap, locale: locale)
   1164     content.sound = UNNotificationSound.default
   1165     content.userInfo = LossyLocalNotification(type: .zap, mention: .note(evId)).to_user_info()
   1166 
   1167     let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
   1168 
   1169     let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
   1170 
   1171     UNUserNotificationCenter.current().add(request) { error in
   1172         if let error = error {
   1173             print("Error: \(error)")
   1174         } else {
   1175             print("Local notification scheduled")
   1176         }
   1177     }
   1178 }