damus

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

commit 2596542cb6b3314fe5716df8f24e2fbc455b9606
parent 915f3901a751239b8c5529ebfa309b8cb1ec8460
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 31 Mar 2023 15:14:55 -0700

Enable offline posting

You can now post, like, repost, reply offline

Changelog-Added: Enable offline posting

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/ContentView.swift | 19+++++++++++--------
Mdamus/Models/Contacts.swift | 4++--
Mdamus/Models/DamusState.swift | 3++-
Mdamus/Models/EventsModel.swift | 2++
Mdamus/Models/FollowersModel.swift | 3+++
Mdamus/Models/FollowingModel.swift | 2++
Mdamus/Models/HomeModel.swift | 4++++
Mdamus/Models/ProfileModel.swift | 2++
Mdamus/Models/SearchHomeModel.swift | 2++
Mdamus/Models/SearchModel.swift | 3+++
Mdamus/Models/ZapsModel.swift | 2++
Mdamus/Nostr/NostrResponse.swift | 22++++++++++++++++++++++
Mdamus/Nostr/RelayPool.swift | 1-
Mdamus/Util/PostBox.swift | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/ActionBar/EventActionBar.swift | 2+-
Mdamus/Views/ConfigView.swift | 2+-
Mdamus/Views/DMChatView.swift | 3++-
Mdamus/Views/EditMetadataView.swift | 2+-
Mdamus/Views/Muting/MutelistView.swift | 2+-
Mdamus/Views/Relays/RecommendedRelayView.swift | 2+-
Mdamus/Views/Relays/RelayDetailView.swift | 4++--
Mdamus/Views/Relays/RelayView.swift | 2+-
Mdamus/Views/ReportView.swift | 12++++++------
Mdamus/Views/SaveKeysView.swift | 10++++++----
25 files changed, 196 insertions(+), 31 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -182,6 +182,7 @@ 4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; }; 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; }; 4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */; }; + 4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F329D779B5005914DB /* PostBox.swift */; }; 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; }; 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; }; 4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; }; @@ -568,6 +569,7 @@ 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; }; 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; }; 4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncedOnChange.swift; sourceTree = "<group>"; }; + 4CE4F0F329D779B5005914DB /* PostBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostBox.swift; sourceTree = "<group>"; }; 4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; }; 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; }; 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; }; @@ -907,6 +909,7 @@ 4C7FF7D628233637009601DB /* Util */ = { isa = PBXGroup; children = ( + 4CE4F0F329D779B5005914DB /* PostBox.swift */, 7C0F392D29B57C8F0039859C /* Extensions */, 4CE879492995B58700F758CC /* Relays */, 4CF0ABEA29844B2F00D66079 /* AnyCodable */, @@ -1630,6 +1633,7 @@ 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */, 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */, 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */, + 4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -234,9 +234,9 @@ struct ContentView: View { func MaybeReportView(target: ReportTarget) -> some View { Group { - if let ds = damus_state { - if let sec = ds.keypair.privkey { - ReportView(pool: ds.pool, target: target, privkey: sec) + if let damus_state { + if let sec = damus_state.keypair.privkey { + ReportView(postbox: damus_state.postbox, target: target, privkey: sec) } else { EmptyView() } @@ -396,7 +396,7 @@ struct ContentView: View { let target = notif.object as! FollowTarget let pk = target.pubkey - if let ev = unfollow_user(pool: damus.pool, + if let ev = unfollow_user(postbox: damus.postbox, our_contacts: damus.contacts.event, pubkey: damus.pubkey, privkey: privkey, @@ -447,7 +447,7 @@ struct ContentView: View { //let to_relays = tup.1 print("post \(post.content)") let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey) - self.damus_state?.pool.send(.event(new_ev)) + self.damus_state?.postbox.send(new_ev) case .cancel: active_sheet = nil print("post cancelled") @@ -502,7 +502,7 @@ struct ContentView: View { } damus_state?.contacts.set_mutelist(mutelist) - ds.pool.send(.event(mutelist)) + ds.postbox.send(mutelist) confirm_overwrite_mutelist = false confirm_block = false @@ -534,7 +534,7 @@ struct ContentView: View { return } damus_state?.contacts.set_mutelist(ev) - ds.pool.send(.event(ev)) + ds.postbox.send(ev) } } }, message: { @@ -615,7 +615,8 @@ struct ContentView: View { relay_metadata: metadatas, drafts: Drafts(), events: EventCache(), - bookmarks: BookmarksManager(pubkey: pubkey) + bookmarks: BookmarksManager(pubkey: pubkey), + postbox: PostBox(pool: pool) ) home.damus_state = self.damus_state! @@ -793,6 +794,8 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f } switch ev { + case .ok: + break case .event(_, let ev): has_event = true callback(ev) diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift @@ -140,7 +140,7 @@ func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, pri return ev } -func unfollow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? { +func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? { guard let cs = our_contacts else { return nil } @@ -149,7 +149,7 @@ func unfollow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, p ev.calculate_id() ev.sign(privkey: privkey) - pool.send(.event(ev)) + postbox.send(ev) return ev } diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift @@ -26,6 +26,7 @@ struct DamusState { let drafts: Drafts let events: EventCache let bookmarks: BookmarksManager + let postbox: PostBox var pubkey: String { return keypair.pubkey @@ -36,6 +37,6 @@ 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: ""), tips: TipCounter(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: "")) + return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(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())) } } diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift @@ -64,6 +64,8 @@ class EventsModel: ObservableObject { handle_event(relay_id: relay_id, ev: ev) case .notice(_): break + case .ok: + break case .eose(_): load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state) } diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift @@ -94,6 +94,9 @@ class FollowersModel: ObservableObject { } else if sub_id == self.profiles_id { damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id]) } + + case .ok: + break } } } diff --git a/damus/Models/FollowingModel.swift b/damus/Models/FollowingModel.swift @@ -58,6 +58,8 @@ class FollowingModel { break case .nostr_event(let nev): switch nev { + case .ok: + break case .event(_, let ev): if ev.kind == 0 { process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -333,7 +333,11 @@ class HomeModel: ObservableObject { self.loading = false break + + case .ok: + break } + } } diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift @@ -133,6 +133,8 @@ class ProfileModel: ObservableObject, Equatable { return } switch resp { + case .ok: + break case .event(_, let ev): add_event(ev) case .notice(let notice): diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift @@ -68,6 +68,8 @@ class SearchHomeModel: ObservableObject { } case .notice(let msg): print("search home notice: \(msg)") + case .ok: + break case .eose(let sub_id): loading = false diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift @@ -131,6 +131,9 @@ func handle_subid_event(pool: RelayPool, relay_id: String, ev: NostrConnectionEv case .event(let ev_subid, let ev): handle(ev_subid, ev) return (ev_subid, false) + + case .ok: + return (nil, false) case .notice(let note): if note.contains("Too many subscription filters") { diff --git a/damus/Models/ZapsModel.swift b/damus/Models/ZapsModel.swift @@ -46,6 +46,8 @@ class ZapsModel: ObservableObject { } switch resp { + case .ok: + break case .notice: break case .eose: diff --git a/damus/Nostr/NostrResponse.swift b/damus/Nostr/NostrResponse.swift @@ -7,13 +7,22 @@ import Foundation +struct CommandResult { + let event_id: String + let ok: Bool + let msg: String +} + enum NostrResponse: Decodable { case event(String, NostrEvent) case notice(String) case eose(String) + case ok(CommandResult) var subid: String? { switch self { + case .ok(_): + return nil case .event(let sub_id, _): return sub_id case .eose(let sub_id): @@ -48,6 +57,19 @@ enum NostrResponse: Decodable { let sub_id = try container.decode(String.self) self = .eose(sub_id) return + } else if typ == "OK" { + var cr: CommandResult + do { + let event_id = try container.decode(String.self) + let ok = try container.decode(Bool.self) + let msg = try container.decode(String.self) + cr = CommandResult(event_id: event_id, ok: ok, msg: msg) + } catch { + print(error) + throw error + } + self = .ok(cr) + //ev.pow = count_hash_leading_zero_bits(ev.id) } throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT or NOTICE, got \(typ)")) diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift @@ -256,7 +256,6 @@ class RelayPool { } } - // handle reconnect logic, etc? for handler in handlers { handler.callback(relay_id, event) } diff --git a/damus/Util/PostBox.swift b/damus/Util/PostBox.swift @@ -6,3 +6,116 @@ // import Foundation + + +class Relayer { + let relay: String + var attempts: Int + var retry_after: Double + var last_attempt: Int64? + + init(relay: String, attempts: Int, retry_after: Double) { + self.relay = relay + self.attempts = attempts + self.retry_after = retry_after + self.last_attempt = nil + } +} + +class PostedEvent { + let event: NostrEvent + var remaining: [Relayer] + + init(event: NostrEvent, remaining: [String]) { + self.event = event + self.remaining = remaining.map { + Relayer(relay: $0, attempts: 0, retry_after: 2.0) + } + } +} + +class PostBox { + let pool: RelayPool + var events: [String: PostedEvent] + + init(pool: RelayPool) { + self.pool = pool + self.events = [:] + pool.register_handler(sub_id: "postbox", handler: handle_event) + } + + func try_flushing_events() { + let now = Int64(Date().timeIntervalSince1970) + for kv in events { + let event = kv.value + for relayer in event.remaining { + if relayer.last_attempt == nil || (now >= (relayer.last_attempt! + Int64(relayer.retry_after))) { + print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds") + flush_event(event, to_relay: relayer) + } + } + } + } + + func handle_event(relay_id: String, _ ev: NostrConnectionEvent) { + try_flushing_events() + + guard case .nostr_event(let resp) = ev else { + return + } + + guard case .ok(let cr) = resp else { + return + } + + remove_relayer(relay_id: relay_id, event_id: cr.event_id) + } + + func remove_relayer(relay_id: String, event_id: String) { + guard let ev = self.events[event_id] else { + return + } + ev.remaining = ev.remaining.filter { + $0.relay != relay_id + } + if ev.remaining.count == 0 { + self.events.removeValue(forKey: event_id) + } + } + + private func flush_event(_ event: PostedEvent, to_relay: Relayer? = nil) { + var relayers = event.remaining + if let to_relay { + relayers = [to_relay] + } + + for relayer in relayers { + relayer.attempts += 1 + relayer.last_attempt = Int64(Date().timeIntervalSince1970) + relayer.retry_after *= 1.5 + pool.send(.event(event.event), to: [relayer.relay]) + } + } + + func flush() { + for event in events { + flush_event(event.value) + } + } + + func send(_ event: NostrEvent) { + // Don't add event if we already have it + if events[event.id] != nil { + return + } + + let remaining = pool.descriptors.map { + $0.url.absoluteString + } + + let posted_ev = PostedEvent(event: event, remaining: remaining) + events[event.id] = posted_ev + + flush_event(posted_ev) + } +} diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift @@ -154,7 +154,7 @@ struct EventActionBar: View { generator.impactOccurred() - damus_state.pool.send(.event(like_ev)) + damus_state.postbox.send(like_ev) } } diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift @@ -293,7 +293,7 @@ struct ConfigView: View { } let ev = created_deleted_account_profile(keypair: full_kp) - state.pool.send(.event(ev)) + state.postbox.send(ev) notify(.logout, ()) } } diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift @@ -130,7 +130,8 @@ struct DMChatView: View { dms.draft = "" - damus_state.pool.send(.event(dm)) + damus_state.postbox.send(dm) + end_editing() } diff --git a/damus/Views/EditMetadataView.swift b/damus/Views/EditMetadataView.swift @@ -102,7 +102,7 @@ struct EditMetadataView: View { let m_metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: metadata) if let metadata_ev = m_metadata_ev { - damus_state.pool.send(.event(metadata_ev)) + damus_state.postbox.send(metadata_ev) } } diff --git a/damus/Views/Muting/MutelistView.swift b/damus/Views/Muting/MutelistView.swift @@ -26,7 +26,7 @@ struct MutelistView: View { } damus_state.contacts.set_mutelist(new_ev) - damus_state.pool.send(.event(new_ev)) + damus_state.postbox.send(new_ev) users = get_mutelist_users(new_ev) } label: { Label(NSLocalizedString("Delete", comment: "Button to remove a user from their blocklist."), systemImage: "trash") diff --git a/damus/Views/Relays/RecommendedRelayView.swift b/damus/Views/Relays/RecommendedRelayView.swift @@ -111,7 +111,7 @@ struct RecommendedRelayView: View { return } process_contact_event(state: damus, ev: ev_after_add) - damus.pool.send(.event(ev_after_add)) + damus.postbox.send(ev_after_add) } } diff --git a/damus/Views/Relays/RelayDetailView.swift b/damus/Views/Relays/RelayDetailView.swift @@ -46,7 +46,7 @@ struct RelayDetailView: View { } process_contact_event(state: state, ev: new_ev) - state.pool.send(.event(new_ev)) + state.postbox.send(new_ev) dismiss() }) { Text("Disconnect From Relay", comment: "Button to disconnect from the relay.") @@ -60,7 +60,7 @@ struct RelayDetailView: View { return } process_contact_event(state: state, ev: ev_after_add) - state.pool.send(.event(ev_after_add)) + state.postbox.send(ev_after_add) dismiss() }) { Text("Connect To Relay", comment: "Button to connect to the relay.") diff --git a/damus/Views/Relays/RelayView.swift b/damus/Views/Relays/RelayView.swift @@ -82,7 +82,7 @@ struct RelayView: View { } process_contact_event(state: state, ev: new_ev) - state.pool.send(.event(new_ev)) + state.postbox.send(new_ev) }) { if showText { Text(NSLocalizedString("Disconnect", comment: "Button to disconnect from a relay server.")) diff --git a/damus/Views/ReportView.swift b/damus/Views/ReportView.swift @@ -8,7 +8,7 @@ import SwiftUI struct ReportView: View { - let pool: RelayPool + let postbox: PostBox let target: ReportTarget let privkey: String @@ -44,7 +44,7 @@ struct ReportView: View { } func do_send_report(type: ReportType) { - guard let ev = send_report(privkey: privkey, pool: pool, target: target, type: type) else { + guard let ev = send_report(privkey: privkey, postbox: postbox, target: target, type: type) else { return } @@ -92,12 +92,12 @@ struct ReportView: View { } } -func send_report(privkey: String, pool: RelayPool, target: ReportTarget, type: ReportType) -> NostrEvent? { +func send_report(privkey: String, postbox: PostBox, target: ReportTarget, type: ReportType) -> NostrEvent? { let report = Report(type: type, target: target, message: "") guard let ev = create_report_event(privkey: privkey, report: report) else { return nil } - pool.send(.event(ev)) + postbox.send(ev) return ev } @@ -106,9 +106,9 @@ struct ReportView_Previews: PreviewProvider { let ds = test_damus_state() VStack { - ReportView(pool: ds.pool, target: ReportTarget.user(""), privkey: "") + ReportView(postbox: ds.postbox, target: ReportTarget.user(""), privkey: "") - ReportView(pool: ds.pool, target: ReportTarget.user(""), privkey: "", report_sent: true, report_id: "report_id") + ReportView(postbox: ds.postbox, target: ReportTarget.user(""), privkey: "", report_sent: true, report_id: "report_id") } } diff --git a/damus/Views/SaveKeysView.swift b/damus/Views/SaveKeysView.swift @@ -107,13 +107,13 @@ struct SaveKeysView: View { switch wsev { case .connected: let metadata = create_account_to_metadata(account) - let m_metadata_ev = make_metadata_event(keypair: account.keypair, metadata: metadata) - let m_contacts_ev = make_first_contact_event(keypair: account.keypair) + let metadata_ev = make_metadata_event(keypair: account.keypair, metadata: metadata) + let contacts_ev = make_first_contact_event(keypair: account.keypair) - if let metadata_ev = m_metadata_ev { + if let metadata_ev { self.pool.send(.event(metadata_ev)) } - if let contacts_ev = m_contacts_ev { + if let contacts_ev { self.pool.send(.event(contacts_ev)) } @@ -141,6 +141,8 @@ struct SaveKeysView: View { print("event in signup?") case .eose: break + case .ok: + break } } }