This is a refactor of the codebase to use a more memory-efficient representation of notes. It should also be much faster at decoding since we're using a custom C json parser now. Changelog-Changed: Improved memory usage and performance when processing events
329 lines
9.1 KiB
Swift
329 lines
9.1 KiB
Swift
//
|
|
// NotificationsModel.swift
|
|
// damus
|
|
//
|
|
// Created by William Casarin on 2023-02-21.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
enum NotificationItem {
|
|
case repost(NoteId, EventGroup)
|
|
case reaction(NoteId, EventGroup)
|
|
case profile_zap(ZapGroup)
|
|
case event_zap(NoteId, ZapGroup)
|
|
case reply(NostrEvent)
|
|
|
|
var is_reply: NostrEvent? {
|
|
if case .reply(let ev) = self {
|
|
return ev
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var is_zap: ZapGroup? {
|
|
switch self {
|
|
case .profile_zap(let zapgrp):
|
|
return zapgrp
|
|
case .event_zap(_, let zapgrp):
|
|
return zapgrp
|
|
case .reaction:
|
|
return nil
|
|
case .reply:
|
|
return nil
|
|
case .repost:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var last_event_at: UInt32 {
|
|
switch self {
|
|
case .reaction(_, let evgrp):
|
|
return evgrp.last_event_at
|
|
case .repost(_, let evgrp):
|
|
return evgrp.last_event_at
|
|
case .profile_zap(let zapgrp):
|
|
return zapgrp.last_event_at
|
|
case .event_zap(_, let zapgrp):
|
|
return zapgrp.last_event_at
|
|
case .reply(let reply):
|
|
return reply.created_at
|
|
}
|
|
}
|
|
|
|
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
|
|
switch self {
|
|
case .repost(_, let evgrp):
|
|
return evgrp.would_filter(isIncluded)
|
|
case .reaction(_, let evgrp):
|
|
return evgrp.would_filter(isIncluded)
|
|
case .profile_zap(let zapgrp):
|
|
return zapgrp.would_filter(isIncluded)
|
|
case .event_zap(_, let zapgrp):
|
|
return zapgrp.would_filter(isIncluded)
|
|
case .reply(let ev):
|
|
return !isIncluded(ev)
|
|
}
|
|
}
|
|
|
|
func filter(_ isIncluded: (NostrEvent) -> Bool) -> NotificationItem? {
|
|
switch self {
|
|
case .repost(let evid, let evgrp):
|
|
return evgrp.filter(isIncluded).map { .repost(evid, $0) }
|
|
case .reaction(let evid, let evgrp):
|
|
return evgrp.filter(isIncluded).map { .reaction(evid, $0) }
|
|
case .profile_zap(let zapgrp):
|
|
return zapgrp.filter(isIncluded).map { .profile_zap($0) }
|
|
case .event_zap(let evid, let zapgrp):
|
|
return zapgrp.filter(isIncluded).map { .event_zap(evid, $0) }
|
|
case .reply(let ev):
|
|
if isIncluded(ev) { return .reply(ev) }
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
class NotificationsModel: ObservableObject, ScrollQueue {
|
|
var incoming_zaps: [Zapping] = []
|
|
var incoming_events: [NostrEvent] = []
|
|
var should_queue: Bool = true
|
|
|
|
// mappings from events to
|
|
var zaps: [NoteId: ZapGroup] = [:]
|
|
var profile_zaps = ZapGroup()
|
|
var reactions: [NoteId: EventGroup] = [:]
|
|
var reposts: [NoteId: EventGroup] = [:]
|
|
var replies: [NostrEvent] = []
|
|
var has_reply = Set<NoteId>()
|
|
var has_ev = Set<NoteId>()
|
|
|
|
@Published var notifications: [NotificationItem] = []
|
|
|
|
func set_should_queue(_ val: Bool) {
|
|
self.should_queue = val
|
|
}
|
|
|
|
func uniq_pubkeys() -> [Pubkey] {
|
|
var pks = Set<Pubkey>()
|
|
|
|
for ev in incoming_events {
|
|
pks.insert(ev.pubkey)
|
|
}
|
|
|
|
for grp in reposts {
|
|
for ev in grp.value.events {
|
|
pks.insert(ev.pubkey)
|
|
}
|
|
}
|
|
|
|
for ev in replies {
|
|
pks.insert(ev.pubkey)
|
|
}
|
|
|
|
for zap in incoming_zaps {
|
|
pks.insert(zap.request.ev.pubkey)
|
|
}
|
|
|
|
return Array(pks)
|
|
}
|
|
|
|
func build_notifications() -> [NotificationItem] {
|
|
var notifs: [NotificationItem] = []
|
|
|
|
for el in zaps {
|
|
let evid = el.key
|
|
let zapgrp = el.value
|
|
|
|
let notif: NotificationItem = .event_zap(evid, zapgrp)
|
|
notifs.append(notif)
|
|
}
|
|
|
|
if !profile_zaps.zaps.isEmpty {
|
|
notifs.append(.profile_zap(profile_zaps))
|
|
}
|
|
|
|
for el in reposts {
|
|
let evid = el.key
|
|
let evgrp = el.value
|
|
|
|
notifs.append(.repost(evid, evgrp))
|
|
}
|
|
|
|
for el in reactions {
|
|
let evid = el.key
|
|
let evgrp = el.value
|
|
|
|
notifs.append(.reaction(evid, evgrp))
|
|
}
|
|
|
|
for reply in replies {
|
|
notifs.append(.reply(reply))
|
|
}
|
|
|
|
notifs.sort { $0.last_event_at > $1.last_event_at }
|
|
return notifs
|
|
}
|
|
|
|
|
|
private func insert_repost(_ ev: NostrEvent, cache: EventCache) -> Bool {
|
|
guard let reposted_ev = ev.get_inner_event(cache: cache) else {
|
|
return false
|
|
}
|
|
|
|
let id = reposted_ev.id
|
|
|
|
if let evgrp = self.reposts[id] {
|
|
return evgrp.insert(ev)
|
|
} else {
|
|
let evgrp = EventGroup()
|
|
self.reposts[id] = evgrp
|
|
return evgrp.insert(ev)
|
|
}
|
|
}
|
|
|
|
private func insert_text(_ ev: NostrEvent) -> Bool {
|
|
guard !has_reply.contains(ev.id) else {
|
|
return false
|
|
}
|
|
|
|
has_reply.insert(ev.id)
|
|
replies.append(ev)
|
|
|
|
return true
|
|
}
|
|
|
|
private func insert_reaction(_ ev: NostrEvent) -> Bool {
|
|
guard let id = ev.referenced_ids.last else {
|
|
return false
|
|
}
|
|
|
|
if let evgrp = self.reactions[id] {
|
|
return evgrp.insert(ev)
|
|
} else {
|
|
let evgrp = EventGroup()
|
|
self.reactions[id] = evgrp
|
|
return evgrp.insert(ev)
|
|
}
|
|
}
|
|
|
|
private func insert_event_immediate(_ ev: NostrEvent, cache: EventCache) -> Bool {
|
|
if ev.known_kind == .boost {
|
|
return insert_repost(ev, cache: cache)
|
|
} else if ev.known_kind == .like {
|
|
return insert_reaction(ev)
|
|
} else if ev.known_kind == .text {
|
|
return insert_text(ev)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private func insert_zap_immediate(_ zap: Zapping) -> Bool {
|
|
switch zap.target {
|
|
case .note(let notezt):
|
|
let id = notezt.note_id
|
|
if let zapgrp = self.zaps[notezt.note_id] {
|
|
return zapgrp.insert(zap)
|
|
} else {
|
|
let zapgrp = ZapGroup()
|
|
self.zaps[id] = zapgrp
|
|
return zapgrp.insert(zap)
|
|
}
|
|
|
|
case .profile:
|
|
return profile_zaps.insert(zap)
|
|
}
|
|
}
|
|
|
|
func insert_event(_ ev: NostrEvent, damus_state: DamusState) -> Bool {
|
|
if has_ev.contains(ev.id) {
|
|
return false
|
|
}
|
|
|
|
if should_queue {
|
|
incoming_events.append(ev)
|
|
has_ev.insert(ev.id)
|
|
return true
|
|
}
|
|
|
|
if insert_event_immediate(ev, cache: damus_state.events) {
|
|
self.notifications = build_notifications()
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func insert_zap(_ zap: Zapping) -> Bool {
|
|
if should_queue {
|
|
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
|
|
}
|
|
|
|
if insert_zap_immediate(zap) {
|
|
self.notifications = build_notifications()
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func filter(_ isIncluded: (NostrEvent) -> Bool) {
|
|
var changed = false
|
|
var count = 0
|
|
|
|
count = incoming_events.count
|
|
incoming_events = incoming_events.filter(isIncluded)
|
|
changed = changed || incoming_events.count != count
|
|
|
|
count = profile_zaps.zaps.count
|
|
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
|
|
changed = changed || profile_zaps.zaps.count != count
|
|
|
|
for el in reactions {
|
|
count = el.value.events.count
|
|
el.value.events = el.value.events.filter(isIncluded)
|
|
changed = changed || el.value.events.count != count
|
|
}
|
|
|
|
for el in reposts {
|
|
count = el.value.events.count
|
|
el.value.events = el.value.events.filter(isIncluded)
|
|
changed = changed || el.value.events.count != count
|
|
}
|
|
|
|
for el in zaps {
|
|
count = el.value.zaps.count
|
|
el.value.zaps = el.value.zaps.filter {
|
|
isIncluded($0.request.ev)
|
|
}
|
|
changed = changed || el.value.zaps.count != count
|
|
}
|
|
|
|
count = replies.count
|
|
replies = replies.filter(isIncluded)
|
|
changed = changed || replies.count != count
|
|
|
|
if changed {
|
|
self.notifications = build_notifications()
|
|
}
|
|
}
|
|
|
|
func flush(_ damus_state: DamusState) -> Bool {
|
|
var inserted = false
|
|
|
|
for zap in incoming_zaps {
|
|
inserted = insert_zap_immediate(zap) || inserted
|
|
}
|
|
|
|
for event in incoming_events {
|
|
inserted = insert_event_immediate(event, cache: damus_state.events) || inserted
|
|
}
|
|
|
|
if inserted {
|
|
self.notifications = build_notifications()
|
|
}
|
|
|
|
return inserted
|
|
}
|
|
}
|