1
0
mirror of git://jb55.com/damus synced 2024-10-06 11:43:21 +00:00

extract HomeModel from ContentView

huge refactor

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin 2022-05-24 12:57:40 -07:00
parent b230d430ee
commit 097cc54bba
27 changed files with 1221 additions and 371 deletions

View File

@ -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;

View File

@ -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()
)
home.damus_state = self.damus_state!
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
}
// 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]
}
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 {

View File

@ -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
}
}

View File

@ -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

View File

@ -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())
}
}

View File

@ -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
}
}
}

View File

@ -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: &notifications, 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("-----")
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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

207
damus/Util/Bech32.swift Normal file
View File

@ -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"
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
)

View File

@ -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))
}
}
}

View File

@ -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
}
}

View File

@ -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()

View File

@ -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 FollowButtonView_Previews: PreviewProvider {
static var previews: some View {
FollowButtonView()
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 {
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
}
}

View File

@ -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)
}
}
}

195
damus/Views/LoginView.swift Normal file
View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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.
}
}
}