damus

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

commit a88324333b2a616c6d9838af67e054cb63de5421
parent ce989450f4fb5ab60b4f1aff56261a4b0eeed166
Author: William Casarin <jb55@jb55.com>
Date:   Sat, 30 Apr 2022 10:37:29 -0700

profiles

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 14++++++++++++++
Mdamus/Assets.xcassets/AppIcon.appiconset/Contents.json | 112++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mdamus/ContentView.swift | 11+++++++----
Adamus/Models/ActionBarModel.swift | 22++++++++++++++++++++++
Adamus/Models/ProfileModel.swift | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Nostr/Nostr.swift | 5-----
Mdamus/Nostr/NostrEvent.swift | 4++++
Adamus/Nostr/NostrKind.swift | 18++++++++++++++++++
Mdamus/Nostr/RelayPool.swift | 12+++++++++++-
Mdamus/Views/ChatView.swift | 6+++---
Mdamus/Views/ProfileName.swift | 2+-
Mdamus/Views/ProfileView.swift | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++----
12 files changed, 286 insertions(+), 65 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -13,6 +13,9 @@ 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; }; 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; }; 4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; }; + 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */; }; + 4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; }; + 4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD5281D995700B3DE84 /* ActionBarModel.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 */; }; @@ -71,6 +74,9 @@ 4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; }; 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; }; 4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; }; + 4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModel.swift; sourceTree = "<group>"; }; + 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrKind.swift; sourceTree = "<group>"; }; + 4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBarModel.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>"; }; @@ -140,6 +146,8 @@ children = ( 4C0A3F8E280F640A000448DE /* ThreadModel.swift */, 4C0A3F92280F66F5000448DE /* ReplyMap.swift */, + 4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */, + 4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */, ); path = Models; sourceTree = "<group>"; @@ -179,6 +187,7 @@ 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */, 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */, 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */, + 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */, ); path = Nostr; sourceTree = "<group>"; @@ -407,13 +416,16 @@ 4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */, 4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */, 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */, + 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */, 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4C8682872814DE470026224F /* ProfileView.swift in Sources */, 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */, + 4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */, 4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */, 4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */, + 4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */, 4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */, 4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */, 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */, @@ -587,6 +599,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = damus/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Damus; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -621,6 +634,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = damus/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Damus; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/damus/Assets.xcassets/AppIcon.appiconset/Contents.json b/damus/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,98 +1,116 @@ { "images" : [ - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" + { + "size" : "20x20", + "idiom": "iphone", + "filename" : "damus2-20@2x.png", + "scale": "2x" }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" + { + "size" : "20x20", + "idiom": "iphone", + "filename" : "damus2-20@3x.png", + "scale": "3x" }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" + { + "size" : "20x20", + "idiom": "ipad", + "filename" : "damus2-20.png", + "scale": "1x" }, { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" + "size" : "20x20", + "idiom": "ipad", + "filename" : "damus2-20@2x.png", + "scale": "2x" }, { + "size" : "29x29", "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" + "filename" : "damus2-29@2x.png", + "scale" : "2x" }, { + "size" : "29x29", "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" + "filename" : "damus2-29@3x.png", + "scale" : "3x" }, { + "size" : "40x40", "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" + "filename" : "damus2-40@2x.png", + "scale" : "2x" }, { + "size" : "40x40", "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" + "filename" : "damus2-40@3x.png", + "scale" : "3x" }, { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" + "size" : "60x60", + "idiom" : "iphone", + "filename" : "damus2-60@2x.png", + "scale" : "2x" }, { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" + "size" : "60x60", + "idiom" : "iphone", + "filename" : "damus2-60@3x.png", + "scale" : "3x" }, { + "size" : "29x29", "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" + "filename" : "damus2-29.png", + "scale" : "1x" }, { + "size" : "29x29", "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" + "filename" : "damus2-29@2x.png", + "scale" : "2x" }, { + "size" : "40x40", "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" + "filename" : "damus2-40.png", + "scale" : "1x" }, { + "size" : "40x40", "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" + "filename" : "damus2-40@2x.png", + "scale" : "2x" }, { + "size" : "76x76", "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" + "filename" : "damus2-76.png", + "scale" : "1x" }, { + "size" : "76x76", "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" + "filename" : "damus2-76@2x.png", + "scale" : "2x" }, { + "size" : "83.5x83.5", "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" + "filename" : "damus2-83.5@2x.png", + "scale" : "2x" }, { + "size" : "1024x1024", "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" + "filename" : "damus2-1024.png", + "scale" : "1x" } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } } diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -44,7 +44,7 @@ struct ContentView: View { @State var status: String = "Not connected" @State var active_sheet: Sheets? = nil @State var profiles: Profiles = Profiles() - @State var active_profile: String? = nil + @State var active_profile: ProfileModel = ProfileModel() @State var friends: [String: ()] = [:] @State var loading: Bool = true @State var pool: RelayPool? = nil @@ -153,7 +153,7 @@ struct ContentView: View { case .home: PostingTimelineView .onAppear() { - switch_timeline(.home) + //switch_timeline(.home) } case .notifications: @@ -175,7 +175,9 @@ struct ContentView: View { .environmentObject(profiles) .padding([.leading, .trailing], 6) - let pv = ProfileView() + let pv = ProfileView(pool: pool) + .environmentObject(active_profile) + .environmentObject(profiles) NavigationLink(destination: tv, isActive: $is_thread_open) { EmptyView() @@ -235,7 +237,7 @@ struct ContentView: View { } .onReceive(handle_notify(.click_profile_pic)) { obj in let pubkey = obj.object as! String - self.active_profile = pubkey + self.active_profile.set_pubkey(pubkey) self.is_profile_open = true } .onReceive(handle_notify(.post)) { obj in @@ -317,6 +319,7 @@ struct ContentView: View { self.pool = pool self.thread.pool = pool + self.active_profile.pool = pool pool.connect() } diff --git a/damus/Models/ActionBarModel.swift b/damus/Models/ActionBarModel.swift @@ -0,0 +1,22 @@ +// +// ActionBarModel.swift +// damus +// +// Created by William Casarin on 2022-04-30. +// + +import Foundation + + +class ActionBarModel: ObservableObject { + @Published var our_like_event: NostrEvent? = nil + @Published var our_boost_event: NostrEvent? = nil + + var liked: Bool { + return our_like_event != nil + } + + var boosted: Bool { + return our_boost_event != nil + } +} diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift @@ -0,0 +1,87 @@ +// +// ProfileModel.swift +// damus +// +// Created by William Casarin on 2022-04-27. +// + +import Foundation + +class ProfileModel: ObservableObject { + @Published var events: [NostrEvent] = [] + @Published var pubkey: String? + var seen_event: Set<String> = Set() + + var sub_id = UUID().description + + var pool: RelayPool? = nil + + deinit { + unsubscribe() + } + + func unsubscribe() { + print("unsubscribing from profile \(pubkey ?? "?") with sub_id \(sub_id)") + pool?.unsubscribe(sub_id: sub_id) + } + + func set_pubkey(_ pk: String) { + if pk == self.pubkey { + return + } + + self.events.removeAll() + self.seen_event.removeAll() + + unsubscribe() + self.sub_id = UUID().description + self.pubkey = pk + subscribe() + } + + func subscribe() { + guard let pubkey = self.pubkey else { + return + } + + let kinds: [Int] = [ + NostrKind.text.rawValue, + NostrKind.delete.rawValue, + NostrKind.boost.rawValue + ] + + var filter = NostrFilter.filter_kinds(kinds) + filter.authors = [pubkey] + + print("subscribing to profile \(pubkey) with sub_id \(sub_id)") + pool?.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event) + } + + func add_event(_ ev: NostrEvent) { + if seen_event.contains(ev.id) { + return + } + if ev.kind == 1 { + self.events.append(ev) + self.events = self.events.sorted { $0.created_at > $1.created_at } + } + seen_event.insert(ev.id) + } + + private func handle_event(relay_id: String, ev: NostrConnectionEvent) { + switch ev { + case .ws_event: + return + case .nostr_event(let resp): + switch resp { + case .event(let sid, let ev): + if sid != self.sub_id { + return + } + add_event(ev) + case .notice(let notice): + notify(.notice, notice) + } + } + } +} diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift @@ -18,11 +18,6 @@ struct Profile: Decodable { } } -enum NostrKind: Int { - case metadata = 0 - case text = 1 -} - enum NostrTag { case other_event(OtherEvent) case key_event(KeyEvent) diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -53,6 +53,10 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible { let p = pow.map { String($0) } ?? "?" return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) pow \(p) content '\(content)' }" } + + var known_kind: NostrKind? { + return NostrKind.init(rawValue: kind) + } private enum CodingKeys: String, CodingKey { case id, sig, tags, pubkey, created_at, kind, content diff --git a/damus/Nostr/NostrKind.swift b/damus/Nostr/NostrKind.swift @@ -0,0 +1,18 @@ +// +// NostrKind.swift +// damus +// +// Created by William Casarin on 2022-04-27. +// + +import Foundation + + +enum NostrKind: Int { + case metadata = 0 + case text = 1 + case contacts = 3 + case delete = 5 + case boost = 6 + case like = 7 +} diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift @@ -90,7 +90,17 @@ class RelayPool { relay.connection.disconnect() } } - + + func unsubscribe(sub_id: String) { + self.remove_handler(sub_id: sub_id) + self.send(.unsubscribe(sub_id)) + } + + func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> ()) { + register_handler(sub_id: sub_id, handler: handler) + send(.subscribe(.init(filters: filters, sub_id: sub_id))) + } + func send(_ req: NostrRequest, to: [String]? = nil) { let relays = to.map{ get_relays($0) } ?? self.relays diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift @@ -107,14 +107,14 @@ struct ChatView: View { } .frame(maxWidth: 32) //} - + Group { VStack(alignment: .leading) { if just_started { HStack { ProfileName(pubkey: event.pubkey, profile: profile) - .foregroundColor(id_to_color(event.pubkey)) - //.shadow(color: Color.secondary, radius: 2, x: 2, y: 2) + .foregroundColor(colorScheme == .dark ? id_to_color(event.pubkey) : Color.black) + //.shadow(color: Color.black, radius: 2) Text("\(format_relative_time(event.created_at))") .foregroundColor(.gray) } diff --git a/damus/Views/ProfileName.swift b/damus/Views/ProfileName.swift @@ -9,7 +9,7 @@ import SwiftUI func ProfileName(pubkey: String, profile: Profile?) -> some View { Text(String(Profile.displayName(profile: profile, pubkey: pubkey))) - .foregroundColor(hex_to_rgb(pubkey)) + //.foregroundColor(hex_to_rgb(pubkey)) .bold() } diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift @@ -7,20 +7,70 @@ import SwiftUI +enum ProfileTab: Hashable { + case posts + case following +} + struct ProfileView: View { - let profile: Profile? = nil + let pool: RelayPool + @State private var selected_tab: ProfileTab = .posts + + @EnvironmentObject var profile: ProfileModel + @EnvironmentObject var profiles: Profiles + + var TopSection: some View { + HStack(alignment: .top) { + let data = profile.pubkey.flatMap { profiles.lookup(id: $0) } + ProfilePicView(picture: data?.picture, size: 64, highlight: .custom(Color.black, 4)) + //.border(Color.blue) + VStack(alignment: .leading) { + if let pubkey = profile.pubkey { + ProfileName(pubkey: pubkey, profile: data) + .font(.title) + //.border(Color.green) + } + Text(data?.about ?? "") + //.border(Color.red) + } + //.border(Color.purple) + //Spacer() + } + //.border(Color.indigo) + } var body: some View { - VStack { - ProfilePicView(picture: profile?.picture, size: 64, highlight: .custom(Color.black, 4)) - //ProfileName(pubkey: <#T##String#>, profile: <#T##Profile?#>) + VStack(alignment: .leading) { + TopSection + Picker("", selection: $selected_tab) { + Text("Posts").tag(ProfileTab.posts) + Text("Following").tag(ProfileTab.following) + } + .pickerStyle(SegmentedPickerStyle()) + + Divider() + + Group { + switch(selected_tab) { + case .posts: + TimelineView(events: $profile.events, pool: pool) + .environmentObject(profiles) + case .following: + Text("Following") + } + } + .frame(maxHeight: .infinity, alignment: .topLeading) } + //.border(Color.white) + .frame(maxWidth: .infinity, alignment: .topLeading) .navigationBarTitle("Profile") } } +/* struct ProfileView_Previews: PreviewProvider { static var previews: some View { ProfileView() } } + */