Merge branch 'master' into user-cache

This commit is contained in:
Bryan Montz
2023-05-26 06:46:47 -05:00
85 changed files with 2379 additions and 299 deletions

View File

@@ -7,12 +7,17 @@
import Foundation
enum Zapped {
case not_zapped
case pending
case zapped
}
class ActionBarModel: ObservableObject {
@Published var our_like: NostrEvent?
@Published var our_boost: NostrEvent?
@Published var our_reply: NostrEvent?
@Published var our_zap: Zap?
@Published var our_zap: Zapping?
@Published var likes: Int
@Published var boosts: Int
@Published var zaps: Int
@@ -35,7 +40,7 @@ class ActionBarModel: ObservableObject {
self.replies = 0
}
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?, our_reply: NostrEvent?) {
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zapping?, our_reply: NostrEvent?) {
self.likes = likes
self.boosts = boosts
self.zaps = zaps
@@ -64,10 +69,6 @@ class ActionBarModel: ObservableObject {
return likes == 0 && boosts == 0 && zaps == 0
}
var zapped: Bool {
return our_zap != nil
}
var liked: Bool {
return our_like != nil
}

View File

@@ -29,9 +29,10 @@ struct DamusState {
let bootstrap_relays: [String]
let replies: ReplyCounter
let muted_threads: MutedThreadsManager
let wallet: WalletModel
@discardableResult
func add_zap(zap: Zap) -> Bool {
func add_zap(zap: Zapping) -> Bool {
// store generic zap mapping
self.zaps.add_zap(zap: zap)
// associate with events as well
@@ -47,5 +48,5 @@ struct DamusState {
}
static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil))) }
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore())) }
}

View File

@@ -129,21 +129,54 @@ class HomeModel: ObservableObject {
handle_zap_event(ev)
case .zap_request:
break
case .nwc_request:
break
case .nwc_response:
handle_nwc_response(ev, relay: relay_id)
}
}
func handle_nwc_response(_ ev: NostrEvent, relay: String) {
Task { @MainActor in
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
let nwc = WalletConnectURL(str: nwc_str),
let resp = await FullWalletResponse(from: ev, nwc: nwc) else {
return
}
// since command results are not returned for ephemeral events,
// remove the request from the postbox which is likely failing over and over
if damus_state.postbox.remove_relayer(relay_id: nwc.relay.id, event_id: resp.req_id) {
print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]")
} else {
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
}
guard let err = resp.response.error else {
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
nwc_success(state: self.damus_state, resp: resp)
return
}
print("nwc error: \(resp.response)")
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
}
}
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return
}
damus_state.add_zap(zap: zap)
damus_state.add_zap(zap: .zap(zap))
guard zap.target.pubkey == our_keypair.pubkey else {
return
}
if !notifications.insert_zap(zap) {
if !notifications.insert_zap(.zap(zap)) {
return
}
@@ -301,6 +334,16 @@ class HomeModel: ObservableObject {
//remove_bootstrap_nodes(damus_state)
send_home_filters(relay_id: relay_id)
}
// connect to nwc relays when connected
if let nwc_str = damus_state.settings.nostr_wallet_connect,
let r = pool.get_relay(relay_id),
r.descriptor.variant == .nwc,
let nwc = WalletConnectURL(str: nwc_str),
nwc.relay.id == relay_id
{
subscribe_to_nwc(url: nwc, pool: pool)
}
case .error(let merr):
let desc = String(describing: merr)
if desc.contains("Software caused connection abort") {
@@ -431,7 +474,7 @@ class HomeModel: ObservableObject {
print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
if let relay_id = relay_id {
if let relay_id {
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id])
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: [relay_id])
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: [relay_id])
@@ -691,7 +734,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
var old_nip05: String? = nil
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
old_nip05 = mprof.profile.nip05
if mprof.timestamp > ev.created_at {
if mprof.event.created_at > ev.created_at {
// skip if we already have an newer profile
return
}
@@ -708,7 +751,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
print("validated nip05 for '\(nip05)'")
}
DispatchQueue.main.async {
Task { @MainActor in
profiles.validated[ev.pubkey] = validated
profiles.nip05_pubkey[nip05] = ev.pubkey
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
@@ -836,7 +879,8 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
changed = true
if new.contains(d) {
if let url = RelayURL(d) {
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, url: url, info: decoded[d] ?? .rw, new_relay_filters: new_relay_filters)
let descriptor = RelayDescriptor(url: url, info: decoded[d] ?? .rw)
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters)
}
} else {
state.pool.remove_relay(d)
@@ -849,8 +893,9 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
}
}
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: RelayURL, info: RelayInfo, new_relay_filters: Bool) {
try? pool.add_relay(url, info: info)
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool) {
try? pool.add_relay(descriptor)
let url = descriptor.url
let relay_id = url.id
guard metadatas.lookup(relay_id: relay_id) == nil else {
@@ -1157,7 +1202,7 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
identifier = "myBoostNotification"
case .like:
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, notify.event.content)
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, to_reaction_emoji(ev: notify.event) ?? "")
identifier = "myLikeNotification"
case .dm:
title = displayName

View File

@@ -8,7 +8,7 @@
import Foundation
class ZapGroup {
var zaps: [Zap]
var zaps: [Zapping]
var msat_total: Int64
var zappers: Set<String>
@@ -17,22 +17,16 @@ class ZapGroup {
return 0
}
return first.event.created_at
return first.created_at
}
func zap_requests() -> [NostrEvent] {
zaps.map { z in
if let priv = z.private_request {
return priv
} else {
return z.request.ev
}
}
zaps.map { z in z.request }
}
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
for zap in zaps {
if !isIncluded(zap.request_ev) {
if !isIncluded(zap.request) {
return true
}
}
@@ -41,7 +35,7 @@ class ZapGroup {
}
func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? {
let new_zaps = zaps.filter { isIncluded($0.request_ev) }
let new_zaps = zaps.filter { isIncluded($0.request) }
guard new_zaps.count > 0 else {
return nil
}
@@ -59,15 +53,15 @@ class ZapGroup {
}
@discardableResult
func insert(_ zap: Zap) -> Bool {
func insert(_ zap: Zapping) -> Bool {
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
return false
}
msat_total += zap.invoice.amount
msat_total += zap.amount
if !zappers.contains(zap.request.ev.pubkey) {
zappers.insert(zap.request.ev.pubkey)
if !zappers.contains(zap.request.pubkey) {
zappers.insert(zap.request.pubkey)
}
return true

View File

@@ -99,7 +99,7 @@ enum NotificationItem {
}
class NotificationsModel: ObservableObject, ScrollQueue {
var incoming_zaps: [Zap]
var incoming_zaps: [Zapping]
var incoming_events: [NostrEvent]
var should_queue: Bool
@@ -150,7 +150,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
for zap in incoming_zaps {
pks.insert(zap.request.ev.pubkey)
pks.insert(zap.request.pubkey)
}
return Array(pks)
@@ -249,7 +249,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
return false
}
private func insert_zap_immediate(_ zap: Zap) -> Bool {
private func insert_zap_immediate(_ zap: Zapping) -> Bool {
switch zap.target {
case .note(let notezt):
let id = notezt.note_id
@@ -285,7 +285,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
return false
}
func insert_zap(_ zap: Zap) -> Bool {
func insert_zap(_ zap: Zapping) -> Bool {
if should_queue {
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
}
@@ -307,7 +307,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
changed = changed || incoming_events.count != count
count = profile_zaps.zaps.count
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request) }
changed = changed || profile_zaps.zaps.count != count
for el in reactions {
@@ -325,7 +325,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
for el in zaps {
count = el.value.zaps.count
el.value.zaps = el.value.zaps.filter {
isIncluded($0.request.ev)
isIncluded($0.request)
}
changed = changed || el.value.zaps.count != count
}

View File

@@ -19,8 +19,11 @@ let fallback_zap_amount = 1000
if let loaded = UserDefaults.standard.object(forKey: self.key) as? T {
self.value = loaded
} else if let loaded = UserDefaults.standard.object(forKey: key) as? T {
// try to load from deprecated non-pubkey-keyed setting
// If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
// migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
self.value = loaded
UserDefaults.standard.set(loaded, forKey: self.key)
UserDefaults.standard.removeObject(forKey: key)
} else {
self.value = default_value
}
@@ -48,8 +51,11 @@ let fallback_zap_amount = 1000
if let loaded = UserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) {
self.value = val
} else if let loaded = UserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) {
// try to load from deprecated non-pubkey-keyed setting
// If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
// migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
self.value = val
UserDefaults.standard.set(val.to_string(), forKey: self.key)
UserDefaults.standard.removeObject(forKey: key)
} else {
self.value = default_value
}
@@ -137,6 +143,9 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
var disable_animation: Bool
@Setting(key: "donation_percent", default_value: 0)
var donation_percent: Int
// Helper for inverse of disable_animation.
// disable_animation was introduced as a setting first, but it's more natural for the settings UI to show the inverse.
@@ -201,6 +210,9 @@ class UserSettingsStore: ObservableObject {
@KeychainStorage(account: "libretranslate_apikey")
var internal_libretranslate_api_key: String?
@KeychainStorage(account: "nostr_wallet_connect")
var nostr_wallet_connect: String? // TODO: strongly type this to WalletConnectURL
var can_translate: Bool {
switch translation_service {

View File

@@ -0,0 +1,64 @@
//
// WalletModel.swift
// damus
//
// Created by William Casarin on 2023-05-09.
//
import Foundation
enum WalletConnectState {
case new(WalletConnectURL)
case existing(WalletConnectURL)
case none
}
class WalletModel: ObservableObject {
var settings: UserSettingsStore
private(set) var previous_state: WalletConnectState
var inital_percent: Int
@Published private(set) var connect_state: WalletConnectState
init(state: WalletConnectState, settings: UserSettingsStore) {
self.connect_state = state
self.previous_state = .none
self.settings = settings
self.inital_percent = settings.donation_percent
}
init(settings: UserSettingsStore) {
self.settings = settings
if let str = settings.nostr_wallet_connect,
let nwc = WalletConnectURL(str: str) {
self.previous_state = .existing(nwc)
self.connect_state = .existing(nwc)
} else {
self.previous_state = .none
self.connect_state = .none
}
self.inital_percent = settings.donation_percent
}
func cancel() {
self.connect_state = previous_state
self.objectWillChange.send()
}
func disconnect() {
self.settings.nostr_wallet_connect = nil
self.connect_state = .none
self.previous_state = .none
}
func new(_ nwc: WalletConnectURL) {
self.connect_state = .new(nwc)
}
func connect(_ nwc: WalletConnectURL) {
self.settings.nostr_wallet_connect = nwc.to_url().absoluteString
notify(.attached_wallet, nwc)
self.connect_state = .existing(nwc)
self.previous_state = .existing(nwc)
}
}

View File

@@ -19,7 +19,7 @@ class ZapsModel: ObservableObject {
self.target = target
}
var zaps: [Zap] {
var zaps: [Zapping] {
return state.events.lookup_zaps(target: target)
}
@@ -53,7 +53,7 @@ class ZapsModel: ObservableObject {
case .notice:
break
case .eose:
let events = state.events.lookup_zaps(target: target).map { $0.request_ev }
let events = state.events.lookup_zaps(target: target).map { $0.request }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
case .event(_, let ev):
guard ev.kind == 9735 else {
@@ -61,22 +61,19 @@ class ZapsModel: ObservableObject {
}
if let zap = state.zaps.zaps[ev.id] {
if state.events.store_zap(zap: zap) {
objectWillChange.send()
}
} else {
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
return
}
if self.state.add_zap(zap: zap) {
objectWillChange.send()
}
state.events.store_zap(zap: zap)
return
}
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
return
}
self.state.add_zap(zap: .zap(zap))
}