diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 4e7e64b..586d472 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ 4C363AA428296DEE006E126D /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA328296DEE006E126D /* SearchModel.swift */; }; 4C363AA828297703006E126D /* InsertSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA728297703006E126D /* InsertSort.swift */; }; 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79A28306D7B00E1F516 /* Contacts.swift */; }; + 4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79C2833036D00E1F516 /* FollowingView.swift */; }; + 4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79E2833115300E1F516 /* FollowButtonView.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 */; }; @@ -117,6 +119,8 @@ 4C363AA328296DEE006E126D /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = ""; }; 4C363AA728297703006E126D /* InsertSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertSort.swift; sourceTree = ""; }; 4C3AC79A28306D7B00E1F516 /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = ""; }; + 4C3AC79C2833036D00E1F516 /* FollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingView.swift; sourceTree = ""; }; + 4C3AC79E2833115300E1F516 /* FollowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowButtonView.swift; sourceTree = ""; }; 4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModel.swift; sourceTree = ""; }; 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrKind.swift; sourceTree = ""; }; 4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBarModel.swift; sourceTree = ""; }; @@ -234,6 +238,8 @@ 4C363A8B28236B92006E126D /* PubkeyView.swift */, 4C363A8D28236FE4006E126D /* NoteContentView.swift */, 4C363AA128296A7E006E126D /* SearchView.swift */, + 4C3AC79C2833036D00E1F516 /* FollowingView.swift */, + 4C3AC79E2833115300E1F516 /* FollowButtonView.swift */, ); path = Views; sourceTree = ""; @@ -481,6 +487,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */, 4C363A8A28236B57006E126D /* MentionView.swift in Sources */, 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */, 4C363AA828297703006E126D /* InsertSort.swift in Sources */, @@ -509,6 +516,7 @@ 4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */, 4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */, 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */, + 4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */, 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */, 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift index dc1fb94..61fadee 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -208,8 +208,7 @@ struct ContentView: View { Group { if let pk = self.active_profile { let profile_model = ProfileModel(pubkey: pk, damus: damus_state!) - let fs = damus_state!.contacts.follow_state(pk) - ProfileView(damus: damus_state!, follow_state: fs, profile: profile_model) + ProfileView(damus_state: damus_state!, profile: profile_model) } else { EmptyView() } diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift index 26fa4e3..731e80f 100644 --- a/damus/Models/ProfileModel.swift +++ b/damus/Models/ProfileModel.swift @@ -9,17 +9,18 @@ import Foundation class ProfileModel: ObservableObject { @Published var events: [NostrEvent] = [] + @Published var contacts: NostrEvent? = nil + @Published var following: Int = 0 + let pubkey: String let damus: DamusState - @Published var following: Bool var seen_event: Set = Set() var sub_id = UUID().description init(pubkey: String, damus: DamusState) { self.pubkey = pubkey self.damus = damus - self.following = damus.contacts.is_friend(pubkey) } func unsubscribe() { @@ -44,13 +45,19 @@ class ProfileModel: ObservableObject { damus.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event) } + func handle_profile_contact_event(_ ev: NostrEvent) { + self.contacts = ev + self.following = count_pubkeys(ev.tags) + } + 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 } + if ev.known_kind == .text { + let _ = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at}) + } else if ev.known_kind == .contacts { + handle_profile_contact_event(ev) } seen_event.insert(ev.id) } @@ -72,3 +79,15 @@ class ProfileModel: ObservableObject { } } } + + +func count_pubkeys(_ tags: [[String]]) -> Int { + var c: Int = 0 + for tag in tags { + if tag.count >= 2 && tag[0] == "p" { + c += 1 + } + } + + return c +} diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift index 058dea2..bfc126c 100644 --- a/damus/Nostr/NostrEvent.swift +++ b/damus/Nostr/NostrEvent.swift @@ -19,10 +19,14 @@ struct KeyEvent { let relay_url: String } -struct ReferencedId { +struct ReferencedId: Identifiable { let ref_id: String let relay_id: String? let key: String + + var id: String { + return ref_id + } } struct EventId: Identifiable, CustomStringConvertible { diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift index 3edef6d..0940889 100644 --- a/damus/Views/EventView.swift +++ b/damus/Views/EventView.swift @@ -69,8 +69,7 @@ struct EventView: View { let profile = damus.profiles.lookup(id: event.pubkey) VStack { let pmodel = ProfileModel(pubkey: event.pubkey, damus: damus) - let fs = damus.contacts.follow_state(event.pubkey) - let pv = ProfileView(damus: damus, follow_state: fs, profile: pmodel) + let pv = ProfileView(damus_state: damus, profile: pmodel) NavigationLink(destination: pv) { ProfilePicView(pubkey: event.pubkey, size: PFP_SIZE!, highlight: highlight, image_cache: damus.image_cache, profiles: damus.profiles) @@ -121,10 +120,16 @@ struct EventView: View { Label("Copy Text", systemImage: "doc.on.doc") } + Button { + UIPasteboard.general.string = "@" + event.pubkey + } label: { + Label("Copy User ID", systemImage: "tag") + } + Button { UIPasteboard.general.string = "&" + event.id } label: { - Label("Copy ID", systemImage: "tag") + Label("Copy Note ID", systemImage: "tag") } Button { diff --git a/damus/Views/FollowButtonView.swift b/damus/Views/FollowButtonView.swift new file mode 100644 index 0000000..fce1f2d --- /dev/null +++ b/damus/Views/FollowButtonView.swift @@ -0,0 +1,44 @@ +// +// FollowButtonView.swift +// damus +// +// Created by William Casarin on 2022-05-16. +// + +import SwiftUI + +struct FollowButtonView: View { + let pubkey: String + @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) + } + .buttonStyle(.bordered) + .onReceive(handle_notify(.followed)) { notif in + let pk = notif.object as! String + if pk != pubkey { + return + } + + self.follow_state = .follows + } + .onReceive(handle_notify(.unfollowed)) { notif in + let pk = notif.object as! String + if pk != pubkey { + return + } + + self.follow_state = .unfollows + } + } +} + + /* +struct FollowButtonView_Previews: PreviewProvider { + static var previews: some View { + FollowButtonView() + } +} + */ diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift new file mode 100644 index 0000000..2af9002 --- /dev/null +++ b/damus/Views/FollowingView.swift @@ -0,0 +1,59 @@ +// +// FollowingView.swift +// damus +// +// Created by William Casarin on 2022-05-16. +// + +import SwiftUI + +struct FollowUserView: View { + let pubkey: String + let damus_state: DamusState + + var body: some View { + HStack(alignment: .top) { + let pmodel = ProfileModel(pubkey: 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) + } + + VStack(alignment: .leading) { + let profile = damus_state.profiles.lookup(id: pubkey) + ProfileName(pubkey: pubkey, profile: profile) + if let about = profile.flatMap { $0.about } { + Text(about) + } + } + + Spacer() + + FollowButtonView(pubkey: pubkey, follow_state: damus_state.contacts.follow_state(pubkey)) + } + } +} + +struct FollowingView: View { + let contact: NostrEvent + let damus_state: DamusState + + var body: some View { + ScrollView { + LazyVStack(alignment: .leading) { + ForEach(contact.referenced_pubkeys) { pk in + FollowUserView(pubkey: pk.ref_id, damus_state: damus_state) + } + } + } + } +} + +/* +struct FollowingView_Previews: PreviewProvider { + static var previews: some View { + FollowingView(contact: <#NostrEvent#>, damus_state: <#DamusState#>) + } +} + */ diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift index 7e3ad74..e5140f7 100644 --- a/damus/Views/ProfileView.swift +++ b/damus/Views/ProfileView.swift @@ -61,9 +61,8 @@ func perform_follow_btn_action(_ fs: FollowState, target: String) -> FollowState } struct ProfileView: View { - let damus: DamusState + let damus_state: DamusState - @State var follow_state: FollowState = .follows @State private var selected_tab: ProfileTab = .posts @StateObject var profile: ProfileModel @@ -71,32 +70,13 @@ struct ProfileView: View { var TopSection: some View { VStack(alignment: .leading) { - let data = damus.profiles.lookup(id: profile.pubkey) + let data = damus_state.profiles.lookup(id: profile.pubkey) HStack(alignment: .top) { - ProfilePicView(pubkey: profile.pubkey, size: PFP_SIZE!, highlight: .custom(Color.black, 2), image_cache: damus.image_cache, profiles: damus.profiles) + ProfilePicView(pubkey: profile.pubkey, size: PFP_SIZE!, highlight: .custom(Color.black, 2), image_cache: damus_state.image_cache, profiles: damus_state.profiles) Spacer() - Button("\(follow_btn_txt(follow_state))") { - follow_state = perform_follow_btn_action(follow_state, target: profile.pubkey) - } - .buttonStyle(.bordered) - .onReceive(handle_notify(.followed)) { notif in - let pk = notif.object as! String - if pk != profile.pubkey { - return - } - - self.follow_state = .follows - } - .onReceive(handle_notify(.unfollowed)) { notif in - let pk = notif.object as! String - if pk != profile.pubkey { - return - } - - self.follow_state = .unfollows - } + FollowButtonView(pubkey: profile.pubkey, follow_state: damus_state.contacts.follow_state(profile.pubkey)) } if let pubkey = profile.pubkey { @@ -108,7 +88,21 @@ struct ProfileView: View { .font(.footnote) .foregroundColor(id_to_color(pubkey)) } + Text(data?.about ?? "") + + if let contact = profile.contacts { + Divider() + + NavigationLink(destination: FollowingView(contact: contact, damus_state: damus_state)) { + HStack { + Text("\(profile.following)") + Text("Following") + .foregroundColor(.gray) + } + } + .buttonStyle(PlainButtonStyle()) + } } } @@ -119,7 +113,7 @@ struct ProfileView: View { Divider() - InnerTimelineView(events: $profile.events, damus: damus) + InnerTimelineView(events: $profile.events, damus: damus_state) } .frame(maxHeight: .infinity, alignment: .topLeading) } @@ -128,7 +122,6 @@ struct ProfileView: View { .navigationBarTitle("Profile") .onAppear() { - follow_state = damus.contacts.follow_state(profile.pubkey) profile.subscribe() } .onDisappear {