Browse Source

following and unfollowing

Signed-off-by: William Casarin <jb55@jb55.com>
profiles-everywhere
William Casarin 2 years ago
parent
commit
7554a87d88
  1. 4
      damus.xcodeproj/project.pbxproj
  2. 53
      damus/ContentView.swift
  3. 122
      damus/Models/Contacts.swift
  4. 1
      damus/Models/DamusState.swift
  5. 2
      damus/Models/ProfileModel.swift
  6. 5
      damus/Nostr/NostrEvent.swift
  7. 2
      damus/Nostr/Relay.swift
  8. 24
      damus/Notifications.swift
  9. 6
      damus/Views/EventView.swift
  10. 71
      damus/Views/ProfileView.swift

4
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 */,

53
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 {
// we should be able to see our own messages in our homefeed
if ev.pubkey == self.pubkey {
return true
}
func is_friend_event(_ ev: NostrEvent) -> Bool {
if is_friend(pubkey: ev.pubkey) {
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 ?? []

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

1
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

2
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() {

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

2
damus/Nostr/Relay.swift

@ -7,7 +7,7 @@
import Foundation
struct RelayInfo {
struct RelayInfo: Codable {
let read: Bool
let write: Bool

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

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

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

Loading…
Cancel
Save