damus

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

HomeModel.swift (42497B)


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