commit 097cc54bba2fc852ea98894864d8fa726cf63c50
parent b230d430ee7776e54285bf8dfe5dab0e8810718b
Author: William Casarin <jb55@jb55.com>
Date: Tue, 24 May 2022 12:57:40 -0700
extract HomeModel from ContentView
huge refactor
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
27 files changed, 1219 insertions(+), 369 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -50,6 +50,9 @@
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDB281DCE6100B3DE84 /* Liked.swift */; };
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */; };
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; };
+ 4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9113283D694D0052CD1C /* FollowTarget.swift */; };
+ 4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C63334F283D40E500B1C9C3 /* HomeModel.swift */; };
+ 4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C633351283D419F00B1C9C3 /* SignalModel.swift */; };
4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; };
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; };
@@ -63,6 +66,9 @@
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
4C8682872814DE470026224F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8682862814DE470026224F /* ProfileView.swift */; };
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */; };
+ 4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
+ 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
+ 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; };
@@ -147,6 +153,9 @@
4C3BEFDB281DCE6100B3DE84 /* Liked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Liked.swift; sourceTree = "<group>"; };
4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusState.swift; sourceTree = "<group>"; };
4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; };
+ 4C5F9113283D694D0052CD1C /* FollowTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowTarget.swift; sourceTree = "<group>"; };
+ 4C63334F283D40E500B1C9C3 /* HomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeModel.swift; sourceTree = "<group>"; };
+ 4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; };
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; };
4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@@ -161,6 +170,9 @@
4C7FF7D42823313F009601DB /* Mentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mentions.swift; sourceTree = "<group>"; };
4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrMetadata.swift; sourceTree = "<group>"; };
+ 4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
+ 4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; };
+ 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profiles.swift; sourceTree = "<group>"; };
@@ -232,6 +244,9 @@
4C363AA328296DEE006E126D /* SearchModel.swift */,
4C3AC79A28306D7B00E1F516 /* Contacts.swift */,
4C285C85283892E7008A31F1 /* CreateAccountModel.swift */,
+ 4C63334F283D40E500B1C9C3 /* HomeModel.swift */,
+ 4C633351283D419F00B1C9C3 /* SignalModel.swift */,
+ 4C5F9113283D694D0052CD1C /* FollowTarget.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -268,6 +283,7 @@
4C285C8328385690008A31F1 /* CreateAccountView.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
+ 4C90BD17283A9EE5008EE7EF /* LoginView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -302,6 +318,7 @@
4C363AA728297703006E126D /* InsertSort.swift */,
4C477C9D282C3A4800033AA3 /* TipCounter.swift */,
4C285C8B28398BC6008A31F1 /* Keys.swift */,
+ 4C90BD19283AA67F008EE7EF /* Bech32.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -355,6 +372,7 @@
4CE6DEF627F7A08200C66700 /* damusTests */ = {
isa = PBXGroup;
children = (
+ 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */,
4C363A9F2828A8DD006E126D /* LikeTests.swift */,
4C363A9D2828A822006E126D /* ReplyTests.swift */,
4CE6DEF727F7A08200C66700 /* damusTests.swift */,
@@ -533,6 +551,7 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
+ 4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
4C363A9828283441006E126D /* TestingPrivate.swift in Sources */,
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
@@ -544,6 +563,7 @@
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
4C363A8428233689006E126D /* Parser.swift in Sources */,
4C363A9A28283854006E126D /* Reply.swift in Sources */,
+ 4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
@@ -558,11 +578,13 @@
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
+ 4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
4C363A94282704FA006E126D /* Post.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
+ 4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
@@ -578,6 +600,7 @@
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
+ 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
@@ -594,6 +617,7 @@
files = (
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
+ 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -31,28 +31,30 @@ enum ThreadState {
}
struct ContentView: View {
- let pubkey: String
- let privkey: String
+ let keypair: Keypair
+
+ var pubkey: String {
+ return keypair.pubkey
+ }
+
+ var privkey: String? {
+ return keypair.privkey
+ }
+
@State var status: String = "Not connected"
@State var active_sheet: Sheets? = nil
- @State var loading: Bool = true
@State var damus_state: DamusState? = nil
@State var selected_timeline: Timeline? = .home
@State var is_thread_open: Bool = false
@State var is_profile_open: Bool = false
- @State var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
- @State var has_events: [String: ()] = [:]
- @State var new_notifications: Bool = false
@State var event: NostrEvent? = nil
- @State var events: [NostrEvent] = []
- @State var friend_events: [NostrEvent] = []
- @State var notifications: [NostrEvent] = []
@State var active_profile: String? = nil
@State var active_search: NostrFilter? = nil
@State var active_event_id: String? = nil
@State var profile_open: Bool = false
@State var thread_open: Bool = false
@State var search_open: Bool = false
+ @StateObject var home: HomeModel = HomeModel()
// connect retry timer
let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
@@ -64,9 +66,10 @@ struct ContentView: View {
HStack {
Spacer()
- if self.loading {
- ProgressView()
- .progressViewStyle(.circular)
+ if home.signal.signal != home.signal.max_signal {
+ Text("\(home.signal.signal)/\(home.signal.max_signal)")
+ .font(.callout)
+ .foregroundColor(.gray)
}
}
@@ -77,10 +80,12 @@ struct ContentView: View {
var PostingTimelineView: some View {
ZStack {
if let damus = self.damus_state {
- TimelineView(events: $friend_events, damus: damus)
+ TimelineView(events: $home.events, damus: damus)
}
- PostButtonContainer {
- self.active_sheet = .post
+ if privkey != nil {
+ PostButtonContainer {
+ self.active_sheet = .post
+ }
}
}
}
@@ -105,13 +110,9 @@ struct ContentView: View {
PostingTimelineView
case .notifications:
- TimelineView(events: $notifications, damus: damus)
+ TimelineView(events: $home.notifications, damus: damus)
.navigationTitle("Notifications")
- case .global:
-
- TimelineView(events: $events, damus: damus)
- .navigationTitle("Global")
case .none:
EmptyView()
}
@@ -165,7 +166,7 @@ struct ContentView: View {
}
}
- TabBar(new_notifications: $new_notifications, selected: $selected_timeline, action: switch_timeline)
+ TabBar(new_notifications: $home.new_notifications, selected: $selected_timeline, action: switch_timeline)
}
.onAppear() {
self.connect()
@@ -201,6 +202,10 @@ struct ContentView: View {
}
.onReceive(handle_notify(.boost)) { notif in
+ guard let privkey = self.privkey else {
+ return
+ }
+
let ev = notif.object as! NostrEvent
let boost = make_boost_event(pubkey: pubkey, privkey: privkey, boosted: ev)
self.damus_state?.pool.send(.event(boost))
@@ -215,6 +220,9 @@ struct ContentView: View {
self.active_sheet = .reply(ev)
}
.onReceive(handle_notify(.like)) { like in
+ guard let privkey = self.privkey else {
+ return
+ }
let ev = like.object as! NostrEvent
let like_ev = make_like_event(pubkey: pubkey, privkey: privkey, liked: ev)
self.damus_state?.pool.send(.event(like_ev))
@@ -224,6 +232,10 @@ struct ContentView: View {
self.damus_state?.pool.send(.event(ev))
}
.onReceive(handle_notify(.unfollow)) { notif in
+ guard let privkey = self.privkey else {
+ return
+ }
+
let pk = notif.object as! String
guard let damus = self.damus_state else {
return
@@ -235,12 +247,16 @@ struct ContentView: View {
privkey: privkey,
unfollow: pk) {
notify(.unfollowed, pk)
- damus.contacts.friends.remove(pk)
+ damus.contacts.remove_friend(pk)
//friend_events = friend_events.filter { $0.pubkey != pk }
}
}
.onReceive(handle_notify(.follow)) { notif in
- let pk = notif.object as! String
+ guard let privkey = self.privkey else {
+ return
+ }
+
+ let fnotify = notif.object as! FollowTarget
guard let damus = self.damus_state else {
return
}
@@ -249,12 +265,22 @@ struct ContentView: View {
our_contacts: damus.contacts.event,
pubkey: damus.pubkey,
privkey: privkey,
- follow: ReferencedId(ref_id: pk, relay_id: nil, key: "p")) {
- notify(.followed, pk)
- damus.contacts.friends.insert(pk)
+ follow: ReferencedId(ref_id: fnotify.pubkey, relay_id: nil, key: "p")) {
+ notify(.followed, fnotify.pubkey)
+
+ switch fnotify {
+ case .pubkey(let pk):
+ damus.contacts.add_friend_pubkey(pk)
+ case .contact(let ev):
+ damus.contacts.add_friend_contact(ev)
+ }
}
}
.onReceive(handle_notify(.post)) { obj in
+ guard let privkey = self.privkey else {
+ return
+ }
+
let post_res = obj.object as! NostrPostResult
switch post_res {
case .post(let post):
@@ -268,12 +294,11 @@ struct ContentView: View {
}
.onReceive(timer) { n in
self.damus_state?.pool.connect_to_disconnected()
- self.loading = (self.damus_state?.pool.num_connecting ?? 0) != 0
}
}
func is_friend_event(_ ev: NostrEvent) -> Bool {
- return damus.is_friend_event(ev, our_pubkey: self.pubkey, friends: self.damus_state!.contacts.friends)
+ return damus.is_friend_event(ev, our_pubkey: self.pubkey, contacts: self.damus_state!.contacts)
}
func switch_timeline(_ timeline: Timeline) {
@@ -283,7 +308,7 @@ struct ContentView: View {
}
if (timeline != .notifications && self.selected_timeline == .notifications) || timeline == .notifications {
- new_notifications = false
+ home.new_notifications = false
}
self.selected_timeline = timeline
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
@@ -312,9 +337,9 @@ struct ContentView: View {
add_relay(pool, "wss://nostr-relay.freeberty.net")
add_relay(pool, "wss://nostr-relay.untethr.me")
- pool.register_handler(sub_id: sub_id, handler: handle_event)
+ pool.register_handler(sub_id: sub_id, handler: home.handle_event)
- self.damus_state = DamusState(pool: pool, pubkey: pubkey,
+ self.damus_state = DamusState(pool: pool, keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(),
@@ -322,239 +347,12 @@ struct ContentView: View {
image_cache: ImageCache(),
profiles: Profiles()
)
- pool.connect()
- }
-
- func handle_contact_event(_ ev: NostrEvent) {
- if ev.pubkey == self.pubkey {
- damus_state!.contacts.event = ev
- // our contacts
- for tag in ev.tags {
- if tag.count > 1 && tag[0] == "p" {
- damus_state!.contacts.friends.insert(tag[1])
- }
- }
- }
- }
-
- func handle_boost_event(_ ev: NostrEvent) {
- var boost_ev_id = ev.last_refid()?.ref_id
-
- // CHECK SIGS ON THESE
- if let inner_ev = ev.inner_event {
- boost_ev_id = inner_ev.id
-
- if inner_ev.kind == 1 {
- handle_text_event(ev)
- }
- }
-
- guard let e = boost_ev_id else {
- return
- }
-
- switch damus_state!.boosts.add_event(ev, target: e) {
- case .already_counted:
- break
- case .success(let n):
- let boosted = Counted(event: ev, id: e, total: n)
- notify(.boosted, boosted)
- }
- }
-
- func handle_like_event(_ ev: NostrEvent) {
- guard let e = ev.last_refid() else {
- // no id ref? invalid like event
- return
- }
+ home.damus_state = self.damus_state!
- // CHECK SIGS ON THESE
-
- switch damus_state!.likes.add_event(ev, target: e.ref_id) {
- case .already_counted:
- break
- case .success(let n):
- let liked = Counted(event: ev, id: e.ref_id, total: n)
- notify(.liked, liked)
- }
- }
-
- func handle_metadata_event(_ ev: NostrEvent) {
- guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
- return
- }
-
- if let mprof = damus_state!.profiles.lookup_with_timestamp(id: ev.pubkey) {
- if mprof.timestamp > ev.created_at {
- // skip if we already have an newer profile
- return
- }
- }
-
- let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at)
- damus_state!.profiles.add(id: ev.pubkey, profile: tprof)
-
- notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
- }
-
- func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
- guard let m = last_event_of_kind[relay_id] else {
- last_event_of_kind[relay_id] = [:]
- return nil
- }
-
- return m[kind]
+ pool.connect()
}
-
- func send_filters(relay_id: String) {
- // TODO: since times should be based on events from a specific relay
- // perhaps we could mark this in the relay pool somehow
- let text_filter = NostrFilter.filter_kinds([1,5,6,7])
- let profile_filter = NostrFilter.filter_profiles
- var contacts_filter = NostrFilter.filter_contacts
-
- contacts_filter.authors = [self.pubkey]
- var filters = [text_filter, profile_filter, contacts_filter]
-
- filters = update_filters_with_since(last_of_kind: last_event_of_kind[relay_id] ?? [:], filters: filters)
-
- print("connected to \(relay_id) with filters:")
- for filter in filters {
- print(filter)
- }
- print("-----")
-
- self.damus_state?.pool.send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: [relay_id])
- //self.pool?.send(.subscribe(.init(filters: [notification_filter], sub_id: "notifications")))
- }
-
- func handle_notification(ev: NostrEvent) {
- notifications.append(ev)
- notifications = notifications.sorted { $0.created_at > $1.created_at }
-
- let last_notified = get_last_notified()
-
- if last_notified == nil || last_notified!.created_at < ev.created_at {
- save_last_notified(ev)
- new_notifications = true
- }
- }
-
- func handle_friend_event(_ ev: NostrEvent) {
- if !is_friend_event(ev) {
- return
- }
- if !insert_uniq_sorted_event(events: &self.friend_events, new_ev: ev, cmp: { $0.created_at > $1.created_at } ) {
- return
- }
- }
-
- func handle_text_event(_ ev: NostrEvent) {
- if should_hide_event(ev) {
- return
- }
-
- if !insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
- return
- }
-
- handle_friend_event(ev)
-
- if is_notification(ev: ev, pubkey: pubkey) {
- handle_notification(ev: ev)
- }
- }
- func process_event(relay_id: String, ev: NostrEvent) {
- if has_events[ev.id] != nil {
- return
- }
-
- has_events[ev.id] = ()
- let last_k = get_last_event_of_kind(relay_id: relay_id, kind: ev.kind)
- if last_k == nil || ev.created_at > last_k!.created_at {
- last_event_of_kind[relay_id]?[ev.kind] = ev
- }
- if ev.kind == 1 {
- handle_text_event(ev)
- } else if ev.kind == 0 {
- handle_metadata_event(ev)
- } else if ev.kind == 6 {
- handle_boost_event(ev)
- } else if ev.kind == 7 {
- handle_like_event(ev)
- } else if ev.kind == 3 {
- handle_contact_event(ev)
-
- if ev.pubkey == pubkey {
- process_friend_events()
- }
- }
- }
-
- func process_friend_events() {
- for event in events {
- handle_friend_event(event)
- }
- }
-
- func handle_event(relay_id: String, conn_event: NostrConnectionEvent) {
- switch conn_event {
- case .ws_event(let ev):
-
- /*
- if let wsev = ws_nostr_event(relay: relay_id, ev: ev) {
- wsev.flags |= 1
- self.events.insert(wsev, at: 0)
- }
- */
-
-
- switch ev {
- case .connected:
- send_filters(relay_id: relay_id)
- case .error(let merr):
- let desc = merr.debugDescription
- if desc.contains("Software caused connection abort") {
- self.damus_state?.pool.reconnect(to: [relay_id])
- }
- case .disconnected: fallthrough
- case .cancelled:
- self.damus_state?.pool.reconnect(to: [relay_id])
- case .reconnectSuggested(let t):
- if t {
- self.damus_state?.pool.reconnect(to: [relay_id])
- }
- default:
- break
- }
-
- self.loading = (self.damus_state?.pool.num_connecting ?? 0) != 0
-
- print("ws_event \(ev)")
-
- case .nostr_event(let ev):
- switch ev {
- case .event(let sub_id, let ev):
- // globally handle likes
- let always_process = ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata
- if !always_process && sub_id != self.sub_id {
- // TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
- return
- }
-
- self.process_event(relay_id: relay_id, ev: ev)
- case .notice(let msg):
- self.events.insert(NostrEvent(content: "NOTICE from \(relay_id): \(msg)", pubkey: "system"), at: 0)
- print(msg)
- }
- }
- }
-
- func should_hide_event(_ ev: NostrEvent) -> Bool {
- return false
- }
}
/*
@@ -574,9 +372,9 @@ func get_metadata_since_time(_ metadata_event: NostrEvent?) -> Int64? {
return metadata_event!.created_at - 60 * 10
}
-func get_since_time(last_event: NostrEvent?) -> Int64 {
+func get_since_time(last_event: NostrEvent?) -> Int64? {
if last_event == nil {
- return Int64(Date().timeIntervalSince1970) - (24 * 60 * 60 * 3)
+ return nil
}
return last_event!.created_at - 60 * 10
@@ -696,7 +494,7 @@ func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrF
return since
}
- return since! < earliest! ? since! : earliest!
+ return earliest.flatMap { earliest in since.map { since in since < earliest ? since : earliest } }
}
if let earliest = earliest {
diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift
@@ -9,9 +9,47 @@ import Foundation
class Contacts {
- var friends: Set<String> = Set()
+ private var friends: Set<String> = Set()
+ private var friend_of_friends: Set<String> = Set()
var event: NostrEvent?
+ func get_friendosphere() -> [String] {
+ var fs = get_friend_list()
+ fs.append(contentsOf: get_friend_of_friend_list())
+ return fs
+ }
+
+ func remove_friend(_ pubkey: String) {
+ friends.remove(pubkey)
+ }
+
+ func get_friend_list() -> [String] {
+ return Array(friends)
+ }
+
+ func get_friend_of_friend_list() -> [String] {
+ return Array(friend_of_friends)
+ }
+
+ func add_friend_pubkey(_ pubkey: String) {
+ friends.insert(pubkey)
+ }
+
+ func add_friend_contact(_ contact: NostrEvent) {
+ friends.insert(contact.pubkey)
+ for friend in contact.referenced_pubkeys {
+ friend_of_friends.insert(friend.ref_id)
+ }
+ }
+
+ func is_friend_of_friend(_ pubkey: String) -> Bool {
+ return friend_of_friends.contains(pubkey)
+ }
+
+ func is_in_friendosphere(_ pubkey: String) -> Bool {
+ return friends.contains(pubkey) || friend_of_friends.contains(pubkey)
+ }
+
func is_friend(_ pubkey: String) -> Bool {
return friends.contains(pubkey)
}
@@ -121,9 +159,9 @@ func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] {
}
// TODO: tests for this
-func is_friend_event(_ ev: NostrEvent, our_pubkey: String, friends: Set<String>) -> Bool
+func is_friend_event(_ ev: NostrEvent, our_pubkey: String, contacts: Contacts) -> Bool
{
- if !friends.contains(ev.pubkey) {
+ if !contacts.is_friend(ev.pubkey) {
return false
}
@@ -133,7 +171,7 @@ func is_friend_event(_ ev: NostrEvent, our_pubkey: String, friends: Set<String>)
// show our replies?
for pk in ev.referenced_pubkeys {
- if friends.contains(pk.ref_id) {
+ if contacts.is_friend(pk.ref_id) {
return true
}
}
diff --git a/damus/Models/CreateAccountModel.swift b/damus/Models/CreateAccountModel.swift
@@ -15,6 +15,14 @@ class CreateAccountModel: ObservableObject {
@Published var pubkey: String = ""
@Published var privkey: String = ""
+ var pubkey_bech32: String {
+ return bech32_pubkey(self.pubkey) ?? ""
+ }
+
+ var privkey_bech32: String {
+ return bech32_privkey(self.privkey) ?? ""
+ }
+
var rendered_name: String {
if real_name.isEmpty {
return nick_name
@@ -29,13 +37,13 @@ class CreateAccountModel: ObservableObject {
init() {
let keypair = generate_new_keypair()
self.pubkey = keypair.pubkey
- self.privkey = keypair.privkey
+ self.privkey = keypair.privkey!
}
init(real: String, nick: String, about: String) {
let keypair = generate_new_keypair()
self.pubkey = keypair.pubkey
- self.privkey = keypair.privkey
+ self.privkey = keypair.privkey!
self.real_name = real
self.nick_name = nick
diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift
@@ -9,11 +9,19 @@ import Foundation
struct DamusState {
let pool: RelayPool
- let pubkey: String
+ let keypair: Keypair
let likes: EventCounter
let boosts: EventCounter
let contacts: Contacts
let tips: TipCounter
let image_cache: ImageCache
let profiles: Profiles
+
+ var pubkey: String {
+ return keypair.pubkey
+ }
+
+ static var empty: DamusState {
+ return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(), tips: TipCounter(our_pubkey: ""), image_cache: ImageCache(), profiles: Profiles())
+ }
}
diff --git a/damus/Models/FollowTarget.swift b/damus/Models/FollowTarget.swift
@@ -0,0 +1,25 @@
+//
+// FollowNotify.swift
+// damus
+//
+// Created by William Casarin on 2022-05-24.
+//
+
+import Foundation
+
+
+enum FollowTarget {
+ case pubkey(String)
+ case contact(NostrEvent)
+
+ var pubkey: String {
+ switch self {
+ case .pubkey(let pk):
+ return pk
+ case .contact(let ev):
+ return ev.pubkey
+ }
+ }
+}
+
+
diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift
@@ -0,0 +1,322 @@
+//
+// HomeModel.swift
+// damus
+//
+// Created by William Casarin on 2022-05-24.
+//
+
+import Foundation
+
+
+class HomeModel: ObservableObject {
+ var damus_state: DamusState
+
+ var has_events: Set<String> = Set()
+ var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
+ var done_init: Bool = false
+
+ let damus_home_subid = UUID().description
+ let damus_contacts_subid = UUID().description
+ let damus_init_subid = UUID().description
+
+ @Published var new_notifications: Bool = false
+ @Published var notifications: [NostrEvent] = []
+ @Published var events: [NostrEvent] = []
+ @Published var signal: SignalModel = SignalModel()
+
+ init() {
+ self.damus_state = DamusState.empty
+ }
+
+ init(damus_state: DamusState) {
+ self.damus_state = damus_state
+ }
+
+ var pool: RelayPool {
+ return damus_state.pool
+ }
+
+ func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
+ if has_events.contains(ev.id) {
+ return
+ }
+
+ has_events.insert(ev.id)
+ let last_k = get_last_event_of_kind(relay_id: relay_id, kind: ev.kind)
+ if last_k == nil || ev.created_at > last_k!.created_at {
+ last_event_of_kind[relay_id]?[ev.kind] = ev
+ }
+ if ev.kind == 1 {
+ handle_text_event(ev)
+ } else if ev.kind == 0 {
+ handle_metadata_event(ev)
+ } else if ev.kind == 6 {
+ handle_boost_event(ev)
+ } else if ev.kind == 7 {
+ handle_like_event(ev)
+ } else if ev.kind == 3 {
+ handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
+ }
+ }
+
+ func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) {
+ load_our_contacts(contacts: self.damus_state.contacts, our_pubkey: self.damus_state.pubkey, ev: ev)
+
+ if sub_id == damus_init_subid {
+ pool.send(.unsubscribe(damus_init_subid), to: [relay_id])
+ if !done_init {
+ done_init = true
+ send_home_filters(relay_id: nil)
+ }
+ }
+ }
+
+ func handle_boost_event(_ ev: NostrEvent) {
+ var boost_ev_id = ev.last_refid()?.ref_id
+
+ // CHECK SIGS ON THESE
+ if let inner_ev = ev.inner_event {
+ boost_ev_id = inner_ev.id
+
+ if inner_ev.kind == 1 {
+ handle_text_event(ev)
+ }
+ }
+
+ guard let e = boost_ev_id else {
+ return
+ }
+
+ switch self.damus_state.boosts.add_event(ev, target: e) {
+ case .already_counted:
+ break
+ case .success(let n):
+ let boosted = Counted(event: ev, id: e, total: n)
+ notify(.boosted, boosted)
+ }
+ }
+
+ func handle_like_event(_ ev: NostrEvent) {
+ guard let e = ev.last_refid() else {
+ // no id ref? invalid like event
+ return
+ }
+
+ // CHECK SIGS ON THESE
+
+ switch damus_state.likes.add_event(ev, target: e.ref_id) {
+ case .already_counted:
+ break
+ case .success(let n):
+ let liked = Counted(event: ev, id: e.ref_id, total: n)
+ notify(.liked, liked)
+ }
+ }
+
+
+ func handle_event(relay_id: String, conn_event: NostrConnectionEvent) {
+ switch conn_event {
+ case .ws_event(let ev):
+
+ /*
+ if let wsev = ws_nostr_event(relay: relay_id, ev: ev) {
+ wsev.flags |= 1
+ self.events.insert(wsev, at: 0)
+ }
+ */
+
+
+ switch ev {
+ case .connected:
+ if !done_init {
+ send_initial_filters(relay_id: relay_id)
+ } else {
+ send_home_filters(relay_id: relay_id)
+ }
+ case .error(let merr):
+ let desc = merr.debugDescription
+ if desc.contains("Software caused connection abort") {
+ pool.reconnect(to: [relay_id])
+ }
+ case .disconnected: fallthrough
+ case .cancelled:
+ pool.reconnect(to: [relay_id])
+ case .reconnectSuggested(let t):
+ if t {
+ pool.reconnect(to: [relay_id])
+ }
+ default:
+ break
+ }
+
+ update_signal_from_pool(signal: self.signal, pool: self.pool)
+
+ print("ws_event \(ev)")
+
+ case .nostr_event(let ev):
+ switch ev {
+ case .event(let sub_id, let ev):
+ // globally handle likes
+ let always_process = sub_id == damus_contacts_subid || sub_id == damus_home_subid || sub_id == damus_init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata
+ if !always_process {
+ // TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
+ return
+ }
+
+ self.process_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
+ case .notice(let msg):
+ //self.events.insert(NostrEvent(content: "NOTICE from \(relay_id): \(msg)", pubkey: "system"), at: 0)
+ print(msg)
+ }
+ }
+ }
+
+
+ /// Send the initial filters, just our contact list mostly
+ func send_initial_filters(relay_id: String) {
+ var filter = NostrFilter.filter_contacts
+ filter.authors = [self.damus_state.pubkey]
+ filter.limit = 1
+
+ pool.send(.subscribe(.init(filters: [filter], sub_id: damus_init_subid)), to: [relay_id])
+ }
+
+ func send_home_filters(relay_id: String?) {
+ // TODO: since times should be based on events from a specific relay
+ // perhaps we could mark this in the relay pool somehow
+
+ var friends = damus_state.contacts.get_friend_list()
+ friends.append(damus_state.pubkey)
+
+ var contacts_filter = NostrFilter.filter_kinds([0,3])
+ contacts_filter.authors = damus_state.contacts.get_friendosphere()
+
+ // TODO: separate likes?
+ var home_filter = NostrFilter.filter_kinds([
+ NostrKind.text.rawValue,
+ NostrKind.like.rawValue,
+ NostrKind.boost.rawValue,
+ ])
+ // include our pubkey as well even if we're not technically a friend
+ home_filter.authors = friends
+ home_filter.limit = 1000
+
+ var home_filters = [home_filter]
+ var contacts_filters = [contacts_filter]
+ let last_of_k = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
+ home_filters = update_filters_with_since(last_of_kind: last_of_k, filters: home_filters)
+ contacts_filters = update_filters_with_since(last_of_kind: last_of_k, filters: contacts_filters)
+
+ print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters])
+
+ if let relay_id = relay_id {
+ pool.send(.subscribe(.init(filters: home_filters, sub_id: damus_home_subid)), to: [relay_id])
+ pool.send(.subscribe(.init(filters: contacts_filters, sub_id: damus_contacts_subid)), to: [relay_id])
+ } else {
+ pool.send(.subscribe(.init(filters: home_filters, sub_id: damus_home_subid)))
+ pool.send(.subscribe(.init(filters: contacts_filters, sub_id: damus_contacts_subid)))
+ }
+ }
+
+ func handle_metadata_event(_ ev: NostrEvent) {
+ guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
+ return
+ }
+
+ if let mprof = damus_state.profiles.lookup_with_timestamp(id: ev.pubkey) {
+ if mprof.timestamp > ev.created_at {
+ // skip if we already have an newer profile
+ return
+ }
+ }
+
+ let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at)
+ damus_state.profiles.add(id: ev.pubkey, profile: tprof)
+
+ notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
+ }
+
+ func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
+ guard let m = last_event_of_kind[relay_id] else {
+ last_event_of_kind[relay_id] = [:]
+ return nil
+ }
+
+ return m[kind]
+ }
+
+ func handle_notification(ev: NostrEvent) {
+ if !insert_uniq_sorted_event(events: ¬ifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
+ return
+ }
+
+ let last_notified = get_last_notified()
+
+ if last_notified == nil || last_notified!.created_at < ev.created_at {
+ save_last_notified(ev)
+ new_notifications = true
+ }
+ }
+
+ func insert_home_event(_ ev: NostrEvent) -> Bool {
+ let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at })
+ return ok
+ }
+
+ func should_hide_event(_ ev: NostrEvent) -> Bool {
+ return false
+ }
+
+ func handle_text_event(_ ev: NostrEvent) {
+ if should_hide_event(ev) {
+ return
+ }
+
+ let _ = insert_home_event(ev)
+
+ if is_notification(ev: ev, pubkey: self.damus_state.pubkey) {
+ handle_notification(ev: ev)
+ }
+ }
+}
+
+
+func update_signal_from_pool(signal: SignalModel, pool: RelayPool) {
+ if signal.max_signal != pool.relays.count {
+ signal.max_signal = pool.relays.count
+ }
+
+ if signal.signal != pool.num_connecting {
+ signal.signal = signal.max_signal - pool.num_connecting
+ }
+}
+
+
+func load_our_contacts(contacts: Contacts, our_pubkey: String, ev: NostrEvent) {
+ if ev.pubkey != our_pubkey {
+ return
+ }
+
+ contacts.event = ev
+
+ // our contacts
+ for tag in ev.tags {
+ if tag.count > 1 && tag[0] == "p" {
+ // TODO: validate pubkey?
+ contacts.add_friend_pubkey(tag[1])
+ }
+ }
+}
+
+
+
+func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
+ let relays = relay_id ?? "relays"
+ print("connected to \(relays) with filters:")
+ for group in groups {
+ for filter in group {
+ print(filter)
+ }
+ }
+ print("-----")
+}
diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift
@@ -18,6 +18,13 @@ class ProfileModel: ObservableObject {
var seen_event: Set<String> = Set()
var sub_id = UUID().description
+ func get_follow_target() -> FollowTarget {
+ if let contacts = contacts {
+ return .contact(contacts)
+ }
+ return .pubkey(pubkey)
+ }
+
init(pubkey: String, damus: DamusState) {
self.pubkey = pubkey
self.damus = damus
@@ -39,7 +46,7 @@ class ProfileModel: ObservableObject {
var filter = NostrFilter.filter_authors([pubkey])
filter.kinds = kinds
- filter.limit = 500
+ filter.limit = 1000
print("subscribing to profile \(pubkey) with sub_id \(sub_id)")
damus.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
diff --git a/damus/Models/SignalModel.swift b/damus/Models/SignalModel.swift
@@ -0,0 +1,32 @@
+//
+// SignalModel.swift
+// damus
+//
+// Created by William Casarin on 2022-05-24.
+//
+
+import Foundation
+
+
+class SignalModel: ObservableObject {
+ @Published var signal: Int
+ @Published var max_signal: Int
+
+ var percentage: Double {
+ if max_signal == 0 {
+ return 0
+ }
+
+ return Double(signal) / Double(max_signal)
+ }
+
+ init() {
+ self.signal = 0
+ self.max_signal = 0
+ }
+
+ init(signal: Int, max_signal: Int) {
+ self.signal = signal
+ self.max_signal = max_signal
+ }
+}
diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift
@@ -349,22 +349,33 @@ func get_referenced_ids(tags: [[String]], key: String) -> [ReferencedId] {
}
}
-func make_first_contact_event(keypair: Keypair) -> NostrEvent {
+func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
+ guard let privkey = keypair.privkey else {
+ return nil
+ }
+
let rw_relay_info = RelayInfo(read: true, write: true)
- let damus_relay = "wss://relay.damus.io"
let relays: [String: RelayInfo] = ["wss://relay.damus.io": rw_relay_info]
let relay_json = encode_json(relays)!
let damus_pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
+ let tags = [
+ ["p", damus_pubkey],
+ ["p", keypair.pubkey] // you're a friend of yourself!
+ ]
let ev = NostrEvent(content: relay_json,
pubkey: keypair.pubkey,
kind: NostrKind.contacts.rawValue,
- tags: [["p", damus_pubkey, damus_relay]])
+ tags: tags)
ev.calculate_id()
- ev.sign(privkey: keypair.privkey)
+ ev.sign(privkey: privkey)
return ev
}
-func make_metadata_event(keypair: Keypair, metadata: NostrMetadata) -> NostrEvent {
+func make_metadata_event(keypair: Keypair, metadata: NostrMetadata) -> NostrEvent? {
+ guard let privkey = keypair.privkey else {
+ return nil
+ }
+
let metadata_json = encode_json(metadata)!
let ev = NostrEvent(content: metadata_json,
pubkey: keypair.pubkey,
@@ -372,7 +383,7 @@ func make_metadata_event(keypair: Keypair, metadata: NostrMetadata) -> NostrEven
tags: [])
ev.calculate_id()
- ev.sign(privkey: keypair.privkey)
+ ev.sign(privkey: privkey)
return ev
}
diff --git a/damus/Nostr/ProofOfWork.swift b/damus/Nostr/ProofOfWork.swift
@@ -75,6 +75,9 @@ func char_to_hex(_ c: UInt8) -> UInt8?
func hex_decode(_ str: String) -> [UInt8]?
{
+ if str.count == 0 {
+ return nil
+ }
var ret: [UInt8] = []
let chars = Array(str.utf8)
var i: Int = 0
diff --git a/damus/Util/Bech32.swift b/damus/Util/Bech32.swift
@@ -0,0 +1,207 @@
+//
+// Bech32.swift
+//
+// Modified by William Casarin in 2022
+// Created by Evolution Group Ltd on 12.02.2018.
+// Copyright © 2018 Evolution Group Ltd. All rights reserved.
+//
+// Base32 address format for native v0-16 witness outputs implementation
+// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
+// Inspired by Pieter Wuille C++ implementation
+import Foundation
+
+/// Bech32 checksum implementation
+fileprivate let gen: [UInt32] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
+/// Bech32 checksum delimiter
+fileprivate let checksumMarker: String = "1"
+/// Bech32 character set for encoding
+fileprivate let encCharset: Data = "qpzry9x8gf2tvdw0s3jn54khce6mua7l".data(using: .utf8)!
+/// Bech32 character set for decoding
+fileprivate let decCharset: [Int8] = [
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
+ -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
+ 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
+ -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
+ 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
+]
+
+ /// Find the polynomial with value coefficients mod the generator as 30-bit.
+public func bech32_polymod(_ values: Data) -> UInt32 {
+ var chk: UInt32 = 1
+ for v in values {
+ let top = (chk >> 25)
+ chk = (chk & 0x1ffffff) << 5 ^ UInt32(v)
+ for i: UInt8 in 0..<5 {
+ chk ^= ((top >> i) & 1) == 0 ? 0 : gen[Int(i)]
+ }
+ }
+ return chk
+ }
+
+
+/// Expand a HRP for use in checksum computation.
+func bech32_expand_hrp(_ s: String) -> Data {
+ var left: [UInt8] = []
+ var right: [UInt8] = []
+ for x in Array(s) {
+ let scalars = String(x).unicodeScalars
+ left.append(UInt8(scalars[scalars.startIndex].value) >> 5)
+ right.append(UInt8(scalars[scalars.startIndex].value) & 31)
+ }
+ return Data(left + [0] + right)
+}
+
+/// Verify checksum
+public func bech32_verify(hrp: String, checksum: Data) -> Bool {
+ var data = bech32_expand_hrp(hrp)
+ data.append(checksum)
+ return bech32_polymod(data) == 1
+}
+
+/// Create checksum
+public func bech32_create_checksum(hrp: String, values: Data) -> Data {
+ var enc = bech32_expand_hrp(hrp)
+ enc.append(values)
+ enc.append(Data(repeating: 0x00, count: 6))
+ let mod: UInt32 = bech32_polymod(enc) ^ 1
+ var ret: Data = Data(repeating: 0x00, count: 6)
+ for i in 0..<6 {
+ ret[i] = UInt8((mod >> (5 * (5 - i))) & 31)
+ }
+ return ret
+}
+
+public func bech32_encode(hrp: String, _ input: [UInt8]) -> String {
+ let table: [Character] = Array("qpzry9x8gf2tvdw0s3jn54khce6mua7l")
+ let bits = eightToFiveBits(input)
+ let check_sum = bech32_checksum(hrp: hrp, data: bits)
+ let separator = "1"
+ return "\(hrp)" + separator + String((bits + check_sum).map { table[Int($0)] })
+}
+
+func bech32_checksum(hrp: String, data: [UInt8]) -> [UInt8] {
+ let values = bech32_expand_hrp(hrp) + data
+ let polymod = bech32_polymod(values + [0,0,0,0,0,0]) ^ 1
+ var result: [UInt32] = []
+ for i in (0..<6) {
+ result.append((polymod >> (5 * (5 - UInt32(i)))) & 31)
+ }
+ return result.map { UInt8($0) }
+}
+
+func eightToFiveBits(_ input: [UInt8]) -> [UInt8] {
+ guard !input.isEmpty else { return [] }
+
+ var outputSize = (input.count * 8) / 5
+ if ((input.count * 8) % 5) != 0 {
+ outputSize += 1
+ }
+ var outputArray: [UInt8] = []
+ for i in (0..<outputSize) {
+ let devision = (i * 5) / 8
+ let reminder = (i * 5) % 8
+ var element = input[devision] << reminder
+ element >>= 3
+
+ if (reminder > 3) && (i + 1 < outputSize) {
+ element = element | (input[devision + 1] >> (8 - reminder + 3))
+ }
+
+ outputArray.append(element)
+ }
+
+ return outputArray
+}
+
+ /// Decode Bech32 string
+public func bech32_decode(_ str: String) throws -> (hrp: String, data: Data) {
+ guard let strBytes = str.data(using: .utf8) else {
+ throw Bech32Error.nonUTF8String
+ }
+ guard strBytes.count <= 90 else {
+ throw Bech32Error.stringLengthExceeded
+ }
+ var lower: Bool = false
+ var upper: Bool = false
+ for c in strBytes {
+ // printable range
+ if c < 33 || c > 126 {
+ throw Bech32Error.nonPrintableCharacter
+ }
+ // 'a' to 'z'
+ if c >= 97 && c <= 122 {
+ lower = true
+ }
+ // 'A' to 'Z'
+ if c >= 65 && c <= 90 {
+ upper = true
+ }
+ }
+ if lower && upper {
+ throw Bech32Error.invalidCase
+ }
+ guard let pos = str.range(of: checksumMarker, options: .backwards)?.lowerBound else {
+ throw Bech32Error.noChecksumMarker
+ }
+ let intPos: Int = str.distance(from: str.startIndex, to: pos)
+ guard intPos >= 1 else {
+ throw Bech32Error.incorrectHrpSize
+ }
+ guard intPos + 7 <= str.count else {
+ throw Bech32Error.incorrectChecksumSize
+ }
+ let vSize: Int = str.count - 1 - intPos
+ var values: Data = Data(repeating: 0x00, count: vSize)
+ for i in 0..<vSize {
+ let c = strBytes[i + intPos + 1]
+ let decInt = decCharset[Int(c)]
+ if decInt == -1 {
+ throw Bech32Error.invalidCharacter
+ }
+ values[i] = UInt8(decInt)
+ }
+ let hrp = String(str[..<pos]).lowercased()
+ guard bech32_verify(hrp: hrp, checksum: values) else {
+ throw Bech32Error.checksumMismatch
+ }
+ return (hrp, Data(values[..<(vSize-6)]))
+}
+
+public enum Bech32Error: LocalizedError {
+ case nonUTF8String
+ case nonPrintableCharacter
+ case invalidCase
+ case noChecksumMarker
+ case incorrectHrpSize
+ case incorrectChecksumSize
+ case stringLengthExceeded
+
+ case invalidCharacter
+ case checksumMismatch
+
+ public var errorDescription: String? {
+ switch self {
+ case .checksumMismatch:
+ return "Checksum doesn't match"
+ case .incorrectChecksumSize:
+ return "Checksum size too low"
+ case .incorrectHrpSize:
+ return "Human-readable-part is too small or empty"
+ case .invalidCase:
+ return "String contains mixed case characters"
+ case .invalidCharacter:
+ return "Invalid character met on decoding"
+ case .noChecksumMarker:
+ return "Checksum delimiter not found"
+ case .nonPrintableCharacter:
+ return "Non printable character in input string"
+ case .nonUTF8String:
+ return "String cannot be decoded by utf8 decoder"
+ case .stringLengthExceeded:
+ return "Input string is too long"
+ }
+ }
+}
diff --git a/damus/Util/InsertSort.swift b/damus/Util/InsertSort.swift
@@ -7,6 +7,16 @@
import Foundation
+func insert_uniq<T: Equatable>(xs: inout [T], new_x: T) -> Bool {
+ for x in xs {
+ if x == new_x {
+ return false
+ }
+ }
+
+ xs.append(new_x)
+ return true
+}
func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool {
var i: Int = 0
diff --git a/damus/Util/Keys.swift b/damus/Util/Keys.swift
@@ -8,9 +8,54 @@
import Foundation
import secp256k1
+let PUBKEY_HRP = "npub"
+let PRIVKEY_HRP = "nsec"
+
struct Keypair {
let pubkey: String
- let privkey: String
+ let privkey: String?
+
+ var pubkey_bech32: String {
+ return bech32_pubkey(pubkey)!
+ }
+
+ var privkey_bech32: String? {
+ return privkey.flatMap { bech32_privkey($0) }
+ }
+}
+
+enum Bech32Key {
+ case pub(String)
+ case sec(String)
+}
+
+func decode_bech32_key(_ key: String) -> Bech32Key? {
+ guard let decoded = try? bech32_decode(key) else {
+ return nil
+ }
+
+ let hexed = hex_encode(decoded.data)
+ if decoded.hrp == "npub" {
+ return .pub(hexed)
+ } else if decoded.hrp == "nsec" {
+ return .sec(hexed)
+ }
+
+ return nil
+}
+
+func bech32_privkey(_ privkey: String) -> String? {
+ guard let bytes = hex_decode(privkey) else {
+ return nil
+ }
+ return bech32_encode(hrp: "nsec", bytes)
+}
+
+func bech32_pubkey(_ pubkey: String) -> String? {
+ guard let bytes = hex_decode(pubkey) else {
+ return nil
+ }
+ return bech32_encode(hrp: "npub", bytes)
}
func generate_new_keypair() -> Keypair {
@@ -21,16 +66,37 @@ func generate_new_keypair() -> Keypair {
return Keypair(pubkey: pubkey, privkey: privkey)
}
-func save_keypair(pubkey: String, privkey: String) {
+func privkey_to_pubkey(privkey: String) -> String? {
+ guard let sec = hex_decode(privkey) else {
+ return nil
+ }
+ guard let key = try? secp256k1.Signing.PrivateKey(rawRepresentation: sec) else {
+ return nil
+ }
+ return hex_encode(Data(key.publicKey.xonlyKeyBytes))
+}
+
+func save_pubkey(pubkey: String) {
UserDefaults.standard.set(pubkey, forKey: "pubkey")
+}
+
+func save_privkey(privkey: String) {
UserDefaults.standard.set(privkey, forKey: "privkey")
}
+func clear_privkey() {
+ UserDefaults.standard.removeObject(forKey: "privkey")
+}
+
+func save_keypair(pubkey: String, privkey: String) {
+ save_pubkey(pubkey: pubkey)
+ save_privkey(privkey: privkey)
+}
+
func get_saved_keypair() -> Keypair? {
get_saved_pubkey().flatMap { pubkey in
- get_saved_privkey().map { privkey in
- return Keypair(pubkey: pubkey, privkey: privkey)
- }
+ let privkey = get_saved_privkey()
+ return Keypair(pubkey: pubkey, privkey: privkey)
}
}
diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift
@@ -115,7 +115,7 @@ struct ChatView: View {
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
let bar = make_actionbar_model(ev: event, damus: damus)
EventActionBar(event: event,
- our_pubkey: damus.pubkey,
+ keypair: damus.keypair,
profiles: damus.profiles,
bar: bar
)
diff --git a/damus/Views/CreateAccountView.swift b/damus/Views/CreateAccountView.swift
@@ -12,32 +12,6 @@ struct CreateAccountView: View {
@State var is_light: Bool = false
@State var is_done: Bool = false
- func FormTextInput(_ title: String, text: Binding<String>) -> some View {
- return TextField("", text: text)
- .placeholder(when: text.wrappedValue.isEmpty) {
- Text(title).foregroundColor(.white.opacity(0.4))
- }
- .padding()
- .background {
- RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
- }
- .foregroundColor(.white)
- .font(.body.bold())
- }
-
- func FormLabel(_ title: String, optional: Bool = false) -> some View {
- return HStack {
- Text(title)
- .bold()
- .foregroundColor(.white)
- if optional {
- Text("optional")
- .font(.callout)
- .foregroundColor(.white.opacity(0.5))
- }
- }
- }
-
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
return VStack(alignment: .leading, spacing: 10.0, content: content)
}
@@ -45,7 +19,7 @@ struct CreateAccountView: View {
func regen_key() {
let keypair = generate_new_keypair()
self.account.pubkey = keypair.pubkey
- self.account.privkey = keypair.privkey
+ self.account.privkey = keypair.privkey!
}
var body: some View {
@@ -89,7 +63,7 @@ struct CreateAccountView: View {
regen_key()
}
- KeyInput($account.pubkey)
+ KeyText($account.pubkey)
.onTapGesture {
regen_key()
}
@@ -110,6 +84,20 @@ struct CreateAccountView: View {
}
.navigationBarTitleDisplayMode(.inline)
+ .navigationBarBackButtonHidden(true)
+ .navigationBarItems(leading: BackNav())
+ }
+}
+
+struct BackNav: View {
+ @Environment(\.dismiss) var dismiss
+
+ var body: some View {
+ Image(systemName: "chevron.backward")
+ .foregroundColor(.white)
+ .onTapGesture {
+ self.dismiss()
+ }
}
}
@@ -133,10 +121,38 @@ struct CreateAccountView_Previews: PreviewProvider {
}
}
-func KeyInput(_ text: Binding<String>) -> some View {
- return Text("\(text.wrappedValue)")
+func KeyText(_ text: Binding<String>) -> some View {
+ let decoded = hex_decode(text.wrappedValue)!
+ let bechkey = bech32_encode(hrp: PUBKEY_HRP, decoded)
+ return Text(bechkey)
.textSelection(.enabled)
.font(.callout.monospaced())
.foregroundColor(.white)
}
+func FormTextInput(_ title: String, text: Binding<String>) -> some View {
+ return TextField("", text: text)
+ .placeholder(when: text.wrappedValue.isEmpty) {
+ Text(title).foregroundColor(.white.opacity(0.4))
+ }
+ .padding()
+ .background {
+ RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
+ }
+ .foregroundColor(.white)
+ .font(.body.bold())
+}
+
+func FormLabel(_ title: String, optional: Bool = false) -> some View {
+ return HStack {
+ Text(title)
+ .bold()
+ .foregroundColor(.white)
+ if optional {
+ Text("optional")
+ .font(.callout)
+ .foregroundColor(.white.opacity(0.5))
+ }
+ }
+}
+
diff --git a/damus/Views/EventActionBar.swift b/damus/Views/EventActionBar.swift
@@ -19,7 +19,7 @@ enum ActionBarSheet: Identifiable {
struct EventActionBar: View {
let event: NostrEvent
- let our_pubkey: String
+ let keypair: Keypair
@State var sheet: ActionBarSheet? = nil
let profiles: Profiles
@StateObject var bar: ActionBarModel
@@ -34,10 +34,12 @@ struct EventActionBar: View {
Spacer()
*/
- EventActionButton(img: "bubble.left", col: nil) {
- notify(.reply, event)
+ if keypair.privkey != nil {
+ EventActionButton(img: "bubble.left", col: nil) {
+ notify(.reply, event)
+ }
+ .padding([.trailing], 20)
}
- .padding([.trailing], 20)
HStack(alignment: .bottom) {
Text("\(bar.likes > 0 ? "\(bar.likes)" : "")")
@@ -90,7 +92,7 @@ struct EventActionBar: View {
return
}
self.bar.likes = liked.total
- if liked.event.pubkey == our_pubkey {
+ if liked.event.pubkey == keypair.pubkey {
self.bar.our_like = liked.event
}
}
diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift
@@ -101,7 +101,7 @@ struct EventView: View {
if has_action_bar {
let bar = make_actionbar_model(ev: event, damus: damus)
- EventActionBar(event: event, our_pubkey: damus.pubkey, profiles: damus.profiles, bar: bar)
+ EventActionBar(event: event, keypair: damus.keypair, profiles: damus.profiles, bar: bar)
}
Divider()
diff --git a/damus/Views/FollowButtonView.swift b/damus/Views/FollowButtonView.swift
@@ -8,17 +8,17 @@
import SwiftUI
struct FollowButtonView: View {
- let pubkey: String
+ let target: FollowTarget
@State var follow_state: FollowState
var body: some View {
Button("\(follow_btn_txt(follow_state))") {
- follow_state = perform_follow_btn_action(follow_state, target: pubkey)
+ follow_state = perform_follow_btn_action(follow_state, target: target)
}
.buttonStyle(.bordered)
.onReceive(handle_notify(.followed)) { notif in
let pk = notif.object as! String
- if pk != pubkey {
+ if pk != target.pubkey {
return
}
@@ -26,7 +26,7 @@ struct FollowButtonView: View {
}
.onReceive(handle_notify(.unfollowed)) { notif in
let pk = notif.object as! String
- if pk != pubkey {
+ if pk != target.pubkey {
return
}
@@ -35,10 +35,43 @@ struct FollowButtonView: View {
}
}
- /*
+struct FollowButtonPreviews: View {
+ let target: FollowTarget = .pubkey("")
+ var body: some View {
+ VStack {
+ Text("Unfollows")
+ FollowButtonView(target: target, follow_state: .unfollows)
+
+ Text("Following")
+ FollowButtonView(target: target, follow_state: .following)
+
+ Text("Follows")
+ FollowButtonView(target: target, follow_state: .follows)
+
+ Text("Unfollowing")
+ FollowButtonView(target: target, follow_state: .unfollowing)
+ }
+ }
+}
+
struct FollowButtonView_Previews: PreviewProvider {
static var previews: some View {
- FollowButtonView()
+ FollowButtonPreviews()
+ }
+}
+
+func perform_follow_btn_action(_ fs: FollowState, target: FollowTarget) -> FollowState {
+ switch fs {
+ case .follows:
+ notify(.unfollow, target)
+ return .following
+ case .following:
+ return .following
+ case .unfollowing:
+ return .following
+ case .unfollows:
+ notify(.follow, target)
+ return .unfollowing
}
}
- */
+
diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift
@@ -8,21 +8,21 @@
import SwiftUI
struct FollowUserView: View {
- let pubkey: String
+ let target: FollowTarget
let damus_state: DamusState
var body: some View {
HStack(alignment: .top) {
- let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
+ let pmodel = ProfileModel(pubkey: target.pubkey, damus: damus_state)
let pv = ProfileView(damus_state: damus_state, profile: pmodel)
NavigationLink(destination: pv) {
- ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, image_cache: damus_state.image_cache, profiles: damus_state.profiles)
+ ProfilePicView(pubkey: target.pubkey, size: PFP_SIZE, highlight: .none, image_cache: damus_state.image_cache, profiles: damus_state.profiles)
}
VStack(alignment: .leading) {
- let profile = damus_state.profiles.lookup(id: pubkey)
- ProfileName(pubkey: pubkey, profile: profile)
+ let profile = damus_state.profiles.lookup(id: target.pubkey)
+ ProfileName(pubkey: target.pubkey, profile: profile)
if let about = profile.flatMap { $0.about } {
Text(about)
}
@@ -30,7 +30,7 @@ struct FollowUserView: View {
Spacer()
- FollowButtonView(pubkey: pubkey, follow_state: damus_state.contacts.follow_state(pubkey))
+ FollowButtonView(target: target, follow_state: damus_state.contacts.follow_state(target.pubkey))
}
}
}
@@ -43,7 +43,7 @@ struct FollowingView: View {
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(contact.referenced_pubkeys) { pk in
- FollowUserView(pubkey: pk.ref_id, damus_state: damus_state)
+ FollowUserView(target: .pubkey(pk.ref_id), damus_state: damus_state)
}
}
}
diff --git a/damus/Views/LoginView.swift b/damus/Views/LoginView.swift
@@ -0,0 +1,195 @@
+//
+// LoginView.swift
+// damus
+//
+// Created by William Casarin on 2022-05-22.
+//
+
+import SwiftUI
+
+enum ParsedKey {
+ case pub(String)
+ case priv(String)
+ case hex(String)
+
+ var is_pub: Bool {
+ if case .pub = self {
+ return true
+ }
+ return false
+ }
+
+ var is_hex: Bool {
+ if case .hex = self {
+ return true
+ }
+ return false
+ }
+}
+
+struct LoginView: View {
+ @State var key: String = ""
+ @State var is_pubkey: Bool = false
+ @State var error: String? = nil
+
+ func get_error(parsed_key: ParsedKey?) -> String? {
+ if self.error != nil {
+ return self.error
+ }
+
+ if !key.isEmpty && parsed_key == nil {
+ return "Invalid key"
+ }
+
+ return nil
+ }
+
+ var body: some View {
+ ZStack(alignment: .top) {
+ DamusGradient()
+ VStack {
+ Text("Login")
+ .foregroundColor(.white)
+ .font(.title)
+ .padding()
+
+ Text("Enter your account key to login:")
+ .foregroundColor(.white)
+ .padding()
+
+ KeyInput("nsec1...", key: $key)
+
+ let parsed = parse_key(key)
+
+ if parsed?.is_hex ?? false {
+ Text("This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key.")
+ .font(.subheadline.bold())
+ .foregroundColor(.white)
+ PubkeySwitch(isOn: $is_pubkey)
+ .padding()
+ }
+
+ if let error = get_error(parsed_key: parsed) {
+ Text(error)
+ .foregroundColor(.red)
+ .padding()
+ }
+
+ if parsed?.is_pub ?? false {
+ Text("This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.")
+ .foregroundColor(.white)
+ .padding()
+ }
+
+ if let p = parsed {
+ DamusWhiteButton("Login") {
+ if !process_login(p, is_pubkey: self.is_pubkey) {
+ self.error = "Invalid key"
+ }
+ }
+ }
+ }
+ .padding()
+ }
+ .navigationBarBackButtonHidden(true)
+ .navigationBarItems(leading: BackNav())
+ }
+}
+
+struct PubkeySwitch: View {
+ @Binding var isOn: Bool
+ var body: some View {
+ HStack {
+ Toggle(isOn: $isOn) {
+ Text("Public Key?")
+ .foregroundColor(.white)
+ }
+ }
+ }
+}
+
+func parse_key(_ thekey: String) -> ParsedKey? {
+ var key = thekey
+ if key.count > 0 && key.first! == "@" {
+ key = String(key.dropFirst())
+ }
+ if hex_decode(key) != nil {
+ return .hex(key)
+ }
+
+ if let bech_key = decode_bech32_key(key) {
+ switch bech_key {
+ case .pub(let pk):
+ return .pub(pk)
+ case .sec(let sec):
+ return .priv(sec)
+ }
+ }
+
+ return nil
+}
+
+func process_login(_ key: ParsedKey, is_pubkey: Bool) -> Bool {
+ switch key {
+ case .priv(let priv):
+ save_privkey(privkey: priv)
+ guard let pk = privkey_to_pubkey(privkey: priv) else {
+ return false
+ }
+ save_pubkey(pubkey: pk)
+
+ case .pub(let pub):
+ clear_privkey()
+ save_pubkey(pubkey: pub)
+
+ case .hex(let hexstr):
+ if is_pubkey {
+ clear_privkey()
+ save_pubkey(pubkey: hexstr)
+ } else {
+ save_privkey(privkey: hexstr)
+ guard let pk = privkey_to_pubkey(privkey: hexstr) else {
+ return false
+ }
+ save_pubkey(pubkey: pk)
+ }
+ }
+
+ notify(.login, ())
+ return true
+}
+
+struct KeyInput: View {
+ let title: String
+ let key: Binding<String>
+
+ init(_ title: String, key: Binding<String>) {
+ self.title = title
+ self.key = key
+ }
+
+ var body: some View {
+ TextField("", text: key)
+ .placeholder(when: key.wrappedValue.isEmpty) {
+ Text(title).foregroundColor(.white.opacity(0.6))
+ }
+ .padding()
+ .background {
+ RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
+ }
+ .autocapitalization(.none)
+ .foregroundColor(.white)
+ .font(.body.monospaced())
+ }
+}
+
+struct LoginView_Previews: PreviewProvider {
+ static var previews: some View {
+ let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
+ let bech32_pubkey = "KeyInput"
+ Group {
+ LoginView(key: pubkey)
+ LoginView(key: bech32_pubkey)
+ }
+ }
+}
diff --git a/damus/Views/MainTabView.swift b/damus/Views/MainTabView.swift
@@ -10,7 +10,6 @@ import SwiftUI
enum Timeline: String, CustomStringConvertible {
case home
case notifications
- case global
case search
var description: String {
@@ -86,7 +85,6 @@ struct TabBar: View {
TabButton(timeline: .home, img: "house", selected: $selected, action: action)
TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, action: action)
NotificationsTab(new_notifications: $new_notifications, selected: $selected, action: action)
- TabButton(timeline: .global, img: "globe.americas", selected: $selected, action: action)
}
}
}
diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift
@@ -45,21 +45,6 @@ func follow_btn_enabled_state(_ fs: FollowState) -> Bool {
}
}
-func perform_follow_btn_action(_ fs: FollowState, target: String) -> FollowState {
- switch fs {
- case .follows:
- notify(.unfollow, target)
- return .following
- case .following:
- return .following
- case .unfollowing:
- return .following
- case .unfollows:
- notify(.follow, target)
- return .unfollowing
- }
-}
-
struct ProfileView: View {
let damus_state: DamusState
@@ -76,7 +61,7 @@ struct ProfileView: View {
Spacer()
- FollowButtonView(pubkey: profile.pubkey, follow_state: damus_state.contacts.follow_state(profile.pubkey))
+ FollowButtonView(target: profile.get_follow_target(), follow_state: damus_state.contacts.follow_state(profile.pubkey))
}
if let pubkey = profile.pubkey {
diff --git a/damus/Views/SaveKeysView.swift b/damus/Views/SaveKeysView.swift
@@ -39,20 +39,22 @@ struct SaveKeysView: View {
.foregroundColor(.white)
.padding(.bottom, 10)
- SaveKeyView(text: account.pubkey, is_copied: $pub_copied)
+ SaveKeyView(text: account.pubkey_bech32, is_copied: $pub_copied)
.padding(.bottom, 10)
- Text("Private Key")
- .font(.title2.bold())
- .foregroundColor(.white)
- .padding(.bottom, 10)
-
- Text("This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!")
- .foregroundColor(.white)
- .padding(.bottom, 10)
-
- SaveKeyView(text: account.privkey, is_copied: $priv_copied)
- .padding(.bottom, 10)
+ if pub_copied {
+ Text("Private Key")
+ .font(.title2.bold())
+ .foregroundColor(.white)
+ .padding(.bottom, 10)
+
+ Text("This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!")
+ .foregroundColor(.white)
+ .padding(.bottom, 10)
+
+ SaveKeyView(text: account.privkey_bech32, is_copied: $priv_copied)
+ .padding(.bottom, 10)
+ }
if pub_copied && priv_copied {
if loading {
@@ -73,6 +75,8 @@ struct SaveKeysView: View {
}
.padding(20)
}
+ .navigationBarBackButtonHidden(true)
+ .navigationBarItems(leading: BackNav())
}
func complete_account_creation(_ account: CreateAccountModel) {
@@ -90,11 +94,15 @@ struct SaveKeysView: View {
switch wsev {
case .connected:
let metadata = create_account_to_metadata(account)
- let metadata_ev = make_metadata_event(keypair: account.keypair, metadata: metadata)
- let contacts_ev = make_first_contact_event(keypair: account.keypair)
+ let m_metadata_ev = make_metadata_event(keypair: account.keypair, metadata: metadata)
+ let m_contacts_ev = make_first_contact_event(keypair: account.keypair)
- self.pool.send(.event(metadata_ev))
- self.pool.send(.event(contacts_ev))
+ if let metadata_ev = m_metadata_ev {
+ self.pool.send(.event(metadata_ev))
+ }
+ if let contacts_ev = m_contacts_ev {
+ self.pool.send(.event(contacts_ev))
+ }
save_keypair(pubkey: account.pubkey, privkey: account.privkey)
notify(.login, account.keypair)
diff --git a/damus/Views/SetupView.swift b/damus/Views/SetupView.swift
@@ -45,6 +45,9 @@ struct SetupView: View {
NavigationLink(destination: CreateAccountView(), tag: .create_account, selection: $state ) {
EmptyView()
}
+ NavigationLink(destination: LoginView(), tag: .login, selection: $state ) {
+ EmptyView()
+ }
Image("logo-nobg")
.resizable()
@@ -64,7 +67,7 @@ struct SetupView: View {
}
Button("Login") {
- notify(.login, ())
+ self.state = .login
}
.padding([.top, .bottom], 20)
.foregroundColor(.white)
diff --git a/damus/damusApp.swift b/damus/damusApp.swift
@@ -26,7 +26,7 @@ struct MainView: View {
var body: some View {
Group {
if let kp = keypair, !needs_setup {
- ContentView(pubkey: kp.pubkey, privkey: kp.privkey)
+ ContentView(keypair: kp)
} else {
SetupView()
.onReceive(handle_notify(.login)) { notif in
diff --git a/damusTests/Bech32Tests.swift b/damusTests/Bech32Tests.swift
@@ -0,0 +1,51 @@
+//
+// Bech32Tests.swift
+// damusTests
+//
+// Created by William Casarin on 2022-05-22.
+//
+
+import XCTest
+@testable import damus
+
+class Bech32Tests: XCTestCase {
+
+ override func setUpWithError() throws {
+ // Put setup code here. This method is called before the invocation of each test method in the class.
+ }
+
+ override func tearDownWithError() throws {
+ // Put teardown code here. This method is called after the invocation of each test method in the class.
+ }
+
+ func test_bech32_encode_decode() throws {
+ // This is an example of a functional test case.
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
+ // Any test you write for XCTest can be annotated as throws and async.
+ // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
+ // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
+
+ let pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
+ guard let b32_pubkey = bech32_pubkey(pubkey) else {
+ XCTAssert(false)
+ return
+ }
+
+ guard let decoded = try? bech32_decode(b32_pubkey) else {
+ XCTAssert(false)
+ return
+ }
+
+ let encoded = hex_encode(decoded.data)
+
+ XCTAssertEqual(encoded, pubkey)
+ }
+
+ func testPerformanceExample() throws {
+ // This is an example of a performance test case.
+ self.measure {
+ // Put the code you want to measure the time of here.
+ }
+ }
+
+}