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 }