commit 7554a87d88d4aa3a576f8d9061c0f0bb6512f7f6
parent 8da251dc88f9846fbce302672435a1c59dfca593
Author: William Casarin <jb55@jb55.com>
Date: Sun, 15 May 2022 11:08:36 -0700
following and unfollowing
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
10 files changed, 277 insertions(+), 15 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -31,6 +31,7 @@
4C363AA228296A7E006E126D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA128296A7E006E126D /* SearchView.swift */; };
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 */; };
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 */; };
@@ -115,6 +116,7 @@
4C363AA128296A7E006E126D /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
4C363AA328296DEE006E126D /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = "<group>"; };
4C363AA728297703006E126D /* InsertSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertSort.swift; sourceTree = "<group>"; };
+ 4C3AC79A28306D7B00E1F516 /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.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>"; };
@@ -204,6 +206,7 @@
4C363A9928283854006E126D /* Reply.swift */,
4C363A9B282838B9006E126D /* EventRef.swift */,
4C363AA328296DEE006E126D /* SearchModel.swift */,
+ 4C3AC79A28306D7B00E1F516 /* Contacts.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -526,6 +529,7 @@
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
+ 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -43,7 +43,6 @@ enum Timeline: String, CustomStringConvertible {
struct ContentView: View {
@State var status: String = "Not connected"
@State var active_sheet: Sheets? = nil
- @State var friends: [String: ()] = [:]
@State var loading: Bool = true
@State var damus: DamusState? = nil
@State var selected_timeline: Timeline? = .home
@@ -209,7 +208,8 @@ struct ContentView: View {
Group {
if let pk = self.active_profile {
let profile_model = ProfileModel(pubkey: pk, damus: damus!)
- ProfileView(damus: damus!, profile: profile_model)
+ let fs = damus!.contacts.follow_state(pk)
+ ProfileView(damus: damus!, follow_state: fs, profile: profile_model)
} else {
EmptyView()
}
@@ -290,6 +290,37 @@ struct ContentView: View {
let ev = obj.object as! NostrEvent
self.damus?.pool.send(.event(ev))
}
+ .onReceive(handle_notify(.unfollow)) { notif in
+ let pk = notif.object as! String
+ guard let damus = self.damus else {
+ return
+ }
+
+ if unfollow_user(pool: damus.pool,
+ our_contacts: damus.contacts.event,
+ pubkey: damus.pubkey,
+ privkey: privkey,
+ unfollow: pk) {
+ notify(.unfollowed, pk)
+ damus.contacts.friends.remove(pk)
+ //friend_events = friend_events.filter { $0.pubkey != pk }
+ }
+ }
+ .onReceive(handle_notify(.follow)) { notif in
+ let pk = notif.object as! String
+ guard let damus = self.damus else {
+ return
+ }
+
+ if follow_user(pool: damus.pool,
+ 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)
+ }
+ }
.onReceive(handle_notify(.post)) { obj in
let post_res = obj.object as! NostrPostResult
switch post_res {
@@ -308,12 +339,13 @@ struct ContentView: View {
}
}
- func is_friend(pubkey: String) -> Bool {
- return pubkey == self.pubkey || friends[pubkey] != nil
- }
-
func is_friend_event(_ ev: NostrEvent) -> Bool {
- if is_friend(pubkey: ev.pubkey) {
+ // we should be able to see our own messages in our homefeed
+ if ev.pubkey == self.pubkey {
+ return true
+ }
+
+ if damus!.contacts.is_friend(ev.pubkey) {
return true
}
@@ -323,7 +355,7 @@ struct ContentView: View {
return true
}
for pk in ev.referenced_pubkeys {
- if is_friend(pubkey: pk.ref_id) {
+ if damus!.contacts.is_friend(pk.ref_id) {
return true
}
}
@@ -366,13 +398,14 @@ struct ContentView: View {
add_relay(pool, "wss://nostr.bitcoiner.social")
add_relay(pool, "ws://monad.jb55.com:8080")
add_relay(pool, "wss://nostr-relay.freeberty.net")
- add_relay(pool, "wss://nostr-relay.untethr.me")
+ //add_relay(pool, "wss://nostr-relay.untethr.me")
pool.register_handler(sub_id: sub_id, handler: handle_event)
self.damus = DamusState(pool: pool, pubkey: pubkey,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
+ contacts: Contacts(),
tips: TipCounter(our_pubkey: pubkey),
image_cache: ImageCache(),
profiles: Profiles()
@@ -382,10 +415,11 @@ struct ContentView: View {
func handle_contact_event(_ ev: NostrEvent) {
if ev.pubkey == self.pubkey {
+ damus!.contacts.event = ev
// our contacts
for tag in ev.tags {
if tag.count > 1 && tag[0] == "p" {
- self.friends[tag[1]] = ()
+ damus!.contacts.friends.insert(tag[1])
}
}
}
@@ -728,7 +762,6 @@ func get_like_pow() -> [String] {
func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
- let now = Int64(Date.now.timeIntervalSince1970)
return filters.map { filter in
let kinds = filter.kinds ?? []
diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift
@@ -0,0 +1,122 @@
+//
+// Contacts.swift
+// damus
+//
+// Created by William Casarin on 2022-05-14.
+//
+
+import Foundation
+
+
+class Contacts {
+ var friends: Set<String> = Set()
+ var event: NostrEvent?
+
+ func is_friend(_ pubkey: String) -> Bool {
+ return friends.contains(pubkey)
+ }
+
+ func follow_state(_ pubkey: String) -> FollowState {
+ return is_friend(pubkey) ? .follows : .unfollows
+ }
+}
+
+
+func create_contacts(relays: [RelayDescriptor], our_pubkey: String, follow: ReferencedId) -> NostrEvent {
+ let kind = NostrKind.contacts.rawValue
+ let content = create_contacts_content(relays) ?? "{}"
+ let tags = [refid_to_tag(follow)]
+ return NostrEvent(content: content, pubkey: our_pubkey, kind: kind, tags: tags)
+}
+
+func create_contacts_content(_ relays: [RelayDescriptor]) -> String? {
+ // TODO: just create a new one of this is corrupted?
+ let crelays = make_contact_relays(relays)
+ guard let encoded = encode_json(crelays) else {
+ return nil
+ }
+ return encoded
+}
+
+
+func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, follow: ReferencedId) -> Bool {
+ guard let ev = follow_user_event(our_contacts: our_contacts, our_pubkey: pubkey, follow: follow) else {
+ return false
+ }
+
+ ev.calculate_id()
+ ev.sign(privkey: privkey)
+
+ pool.send(.event(ev))
+
+ return true
+}
+
+func unfollow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> Bool {
+ guard let cs = our_contacts else {
+ return false
+ }
+
+ let ev = unfollow_user_event(our_contacts: cs, our_pubkey: pubkey, unfollow: unfollow)
+ ev.calculate_id()
+ ev.sign(privkey: privkey)
+
+ pool.send(.event(ev))
+
+ return true
+}
+
+func unfollow_user_event(our_contacts: NostrEvent, our_pubkey: String, unfollow: String) -> NostrEvent {
+ let tags = our_contacts.tags.filter { tag in
+ if tag.count >= 2 && tag[0] == "p" && tag[1] == unfollow {
+ return false
+ }
+ return true
+ }
+
+ let kind = NostrKind.contacts.rawValue
+ return NostrEvent(content: our_contacts.content, pubkey: our_pubkey, kind: kind, tags: tags)
+}
+
+func follow_user_event(our_contacts: NostrEvent?, our_pubkey: String, follow: ReferencedId) -> NostrEvent? {
+ guard let cs = our_contacts else {
+ // don't create contacts for now so we don't nuke our contact list due to connectivity issues
+ // we should only create contacts during profile creation
+ //return create_contacts(relays: relays, our_pubkey: our_pubkey, follow: follow)
+ return nil
+ }
+
+ guard let ev = follow_with_existing_contacts(our_pubkey: our_pubkey, our_contacts: cs, follow: follow) else {
+ return nil
+ }
+
+ return ev
+}
+
+/*
+func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [String: RelayInfo] {
+ guard let relay_info = decode_json_relays(content) else {
+ return make_contact_relays(relays)
+ }
+ return relay_info
+}
+ */
+
+func follow_with_existing_contacts(our_pubkey: String, our_contacts: NostrEvent, follow: ReferencedId) -> NostrEvent? {
+ // don't update if we're already following
+ if our_contacts.references(id: follow.ref_id, key: "p") {
+ return nil
+ }
+
+ let kind = NostrKind.contacts.rawValue
+ var tags = our_contacts.tags
+ tags.append(refid_to_tag(follow))
+ return NostrEvent(content: our_contacts.content, pubkey: our_pubkey, kind: kind, tags: tags)
+}
+
+func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] {
+ return relays.reduce(into: [:]) { acc, relay in
+ acc[relay.url.absoluteString] = relay.info
+ }
+}
+
diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift
@@ -12,6 +12,7 @@ struct DamusState {
let pubkey: String
let likes: EventCounter
let boosts: EventCounter
+ let contacts: Contacts
let tips: TipCounter
let image_cache: ImageCache
let profiles: Profiles
diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift
@@ -12,12 +12,14 @@ class ProfileModel: ObservableObject {
let pubkey: String
let damus: DamusState
+ @Published var following: Bool
var seen_event: Set<String> = 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() {
diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift
@@ -227,6 +227,11 @@ func decode_nostr_event(txt: String) -> NostrResponse? {
return decode_data(Data(txt.utf8))
}
+func encode_json<T: Encodable>(_ val: T) -> String? {
+ let encoder = JSONEncoder()
+ return (try? encoder.encode(val)).map { String(decoding: $0, as: UTF8.self) }
+}
+
func decode_data<T: Decodable>(_ data: Data) -> T? {
let decoder = JSONDecoder()
do {
diff --git a/damus/Nostr/Relay.swift b/damus/Nostr/Relay.swift
@@ -7,7 +7,7 @@
import Foundation
-struct RelayInfo {
+struct RelayInfo: Codable {
let read: Bool
let write: Bool
diff --git a/damus/Notifications.swift b/damus/Notifications.swift
@@ -109,6 +109,30 @@ extension Notification.Name {
}
}
+extension Notification.Name {
+ static var follow: Notification.Name {
+ return Notification.Name("follow")
+ }
+}
+
+extension Notification.Name {
+ static var unfollow: Notification.Name {
+ return Notification.Name("unfollow")
+ }
+}
+
+extension Notification.Name {
+ static var followed: Notification.Name {
+ return Notification.Name("followed")
+ }
+}
+
+extension Notification.Name {
+ static var unfollowed: Notification.Name {
+ return Notification.Name("unfollowed")
+ }
+}
+
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
return NotificationCenter.default.publisher(for: name)
}
diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift
@@ -68,7 +68,9 @@ struct EventView: View {
return HStack {
let profile = damus.profiles.lookup(id: event.pubkey)
VStack {
- let pv = ProfileView(damus: damus, profile: ProfileModel(pubkey: event.pubkey, damus: damus))
+ 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)
NavigationLink(destination: pv) {
ProfilePicView(pubkey: event.pubkey, size: PFP_SIZE!, highlight: highlight, image_cache: damus.image_cache, profiles: damus.profiles)
@@ -196,3 +198,5 @@ func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel {
our_tip: our_tip
)
}
+
+
diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift
@@ -12,9 +12,58 @@ enum ProfileTab: Hashable {
case following
}
+enum FollowState {
+ case follows
+ case following
+ case unfollowing
+ case unfollows
+}
+
+func follow_btn_txt(_ fs: FollowState) -> String {
+ switch fs {
+ case .follows:
+ return "Unfollow"
+ case .following:
+ return "Following..."
+ case .unfollowing:
+ return "Unfollowing..."
+ case .unfollows:
+ return "Follow"
+ }
+}
+
+func follow_btn_enabled_state(_ fs: FollowState) -> Bool {
+ switch fs {
+ case .follows:
+ return true
+ case .following:
+ return false
+ case .unfollowing:
+ return false
+ case .unfollows:
+ return true
+ }
+}
+
+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: DamusState
+ @State var follow_state: FollowState = .follows
@State private var selected_tab: ProfileTab = .posts
@StateObject var profile: ProfileModel
@@ -28,8 +77,25 @@ struct ProfileView: View {
Spacer()
- Button("Follow") {
- print("follow \(profile.pubkey)")
+ 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
}
}
@@ -62,6 +128,7 @@ struct ProfileView: View {
.navigationBarTitle("Profile")
.onAppear() {
+ follow_state = damus.contacts.follow_state(profile.pubkey)
profile.subscribe()
}
.onDisappear {