commit f42bc2e91e034cec21fc531b7e4f68600c229757
parent 403fa74f8d5ada2bb658874610ab3285a3aa7065
Author: William Casarin <jb55@jb55.com>
Date: Wed, 4 May 2022 16:26:10 -0700
likes, mention parsing, lots of stuff
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
25 files changed, 608 insertions(+), 118 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -13,9 +13,14 @@
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 */; };
+ 4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.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 */; };
+ 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */; };
+ 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDB281DCE6100B3DE84 /* Liked.swift */; };
+ 4C3BEFDE281DD59C00B3DE84 /* ParsedRefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDD281DD59C00B3DE84 /* ParsedRefs.swift */; };
+ 4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDF281DE1ED00B3DE84 /* DamusState.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 */; };
@@ -26,6 +31,7 @@
4C75EFB728049D990006080F /* RelayPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB628049D990006080F /* RelayPool.swift */; };
4C75EFB92804A2740006080F /* EventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB82804A2740006080F /* EventView.swift */; };
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */; };
+ 4C7FF7D52823313F009601DB /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
4C8682872814DE470026224F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8682862814DE470026224F /* ProfileView.swift */; };
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
@@ -74,9 +80,14 @@
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>"; };
+ 4C363A8328233689006E126D /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.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>"; };
+ 4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeCounter.swift; sourceTree = "<group>"; };
+ 4C3BEFDB281DCE6100B3DE84 /* Liked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Liked.swift; sourceTree = "<group>"; };
+ 4C3BEFDD281DD59C00B3DE84 /* ParsedRefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsedRefs.swift; sourceTree = "<group>"; };
+ 4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusState.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>"; };
@@ -88,6 +99,7 @@
4C75EFB628049D990006080F /* RelayPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPool.swift; sourceTree = "<group>"; };
4C75EFB82804A2740006080F /* EventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventView.swift; sourceTree = "<group>"; };
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = "<group>"; };
+ 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>"; };
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>"; };
@@ -148,6 +160,11 @@
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */,
4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */,
+ 4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */,
+ 4C3BEFDB281DCE6100B3DE84 /* Liked.swift */,
+ 4C3BEFDD281DD59C00B3DE84 /* ParsedRefs.swift */,
+ 4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */,
+ 4C7FF7D42823313F009601DB /* Mentions.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -192,6 +209,14 @@
path = Nostr;
sourceTree = "<group>";
};
+ 4C7FF7D628233637009601DB /* Util */ = {
+ isa = PBXGroup;
+ children = (
+ 4C363A8328233689006E126D /* Parser.swift */,
+ );
+ path = Util;
+ sourceTree = "<group>";
+ };
4CE6DEDA27F7A08100C66700 = {
isa = PBXGroup;
children = (
@@ -216,6 +241,7 @@
4CE6DEE527F7A08100C66700 /* damus */ = {
isa = PBXGroup;
children = (
+ 4C7FF7D628233637009601DB /* Util */,
4C0A3F8D280F63FF000448DE /* Models */,
4C75EFAB28049CC80006080F /* Nostr */,
4C75EFA72804823E0006080F /* Info.plist */,
@@ -407,14 +433,19 @@
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
+ 4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
+ 4C363A8428233689006E126D /* Parser.swift in Sources */,
+ 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
+ 4C3BEFDE281DD59C00B3DE84 /* ParsedRefs.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
+ 4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
@@ -429,6 +460,7 @@
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */,
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
+ 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -46,7 +46,7 @@ struct ContentView: View {
@State var profiles: Profiles = Profiles()
@State var friends: [String: ()] = [:]
@State var loading: Bool = true
- @State var pool: RelayPool? = nil
+ @State var damus: DamusState? = nil
@State var selected_timeline: Timeline? = .home
@State var is_thread_open: Bool = false
@State var is_profile_open: Bool = false
@@ -135,15 +135,15 @@ struct ContentView: View {
var PostingTimelineView: some View {
ZStack {
- if let pool = self.pool {
- TimelineView(events: $friend_events, pool: pool)
+ if let damus = self.damus {
+ TimelineView(events: $friend_events, damus: damus)
.environmentObject(profiles)
}
PostButtonContainer
}
}
- func MainContent(pool: RelayPool) -> some View {
+ func MainContent(damus: DamusState) -> some View {
NavigationView {
VStack {
switch selected_timeline {
@@ -154,13 +154,13 @@ struct ContentView: View {
}
case .notifications:
- TimelineView(events: $notifications, pool: pool)
+ TimelineView(events: $notifications, damus: damus)
.environmentObject(profiles)
.navigationTitle("Notifications")
case .global:
- TimelineView(events: $events, pool: pool)
+ TimelineView(events: $events, damus: damus)
.environmentObject(profiles)
.navigationTitle("Global")
case .none:
@@ -174,9 +174,9 @@ struct ContentView: View {
var body: some View {
VStack {
- if let pool = self.pool {
+ if let damus = self.damus {
ZStack {
- MainContent(pool: pool)
+ MainContent(damus: damus)
.padding([.bottom], -8.0)
LoadingContainer
@@ -193,14 +193,14 @@ struct ContentView: View {
case .post:
PostView(references: [])
case .reply(let event):
- ReplyView(replying_to: event, pool: pool!)
+ ReplyView(replying_to: event, damus: damus!)
.environmentObject(profiles)
}
}
.onReceive(handle_notify(.boost)) { notif in
let ev = notif.object as! NostrEvent
let boost = make_boost_event(ev, privkey: privkey, pubkey: pubkey)
- self.pool?.send(.event(boost))
+ self.damus?.pool.send(.event(boost))
}
.onReceive(handle_notify(.open_thread)) { obj in
//let ev = obj.object as! NostrEvent
@@ -211,9 +211,18 @@ struct ContentView: View {
let ev = notif.object as! NostrEvent
self.active_sheet = .reply(ev)
}
+ .onReceive(handle_notify(.like)) { like in
+ let ev = like.object as! NostrEvent
+ guard let like_ev = make_like_event(pubkey: pubkey, liked: ev) else {
+ return
+ }
+ like_ev.calculate_id()
+ like_ev.sign(privkey: privkey)
+ self.damus?.pool.send(.event(like_ev))
+ }
.onReceive(handle_notify(.broadcast_event)) { obj in
let ev = obj.object as! NostrEvent
- self.pool?.send(.event(ev))
+ self.damus?.pool.send(.event(ev))
}
.onReceive(handle_notify(.post)) { obj in
let post_res = obj.object as! NostrPostResult
@@ -221,15 +230,15 @@ struct ContentView: View {
case .post(let post):
print("post \(post.content)")
let new_ev = post.to_event(privkey: privkey, pubkey: pubkey)
- self.pool?.send(.event(new_ev))
+ self.damus?.pool.send(.event(new_ev))
case .cancel:
active_sheet = nil
print("post cancelled")
}
}
.onReceive(timer) { n in
- self.pool?.connect_to_disconnected()
- self.loading = (self.pool?.num_connecting ?? 0) != 0
+ self.damus?.pool.connect_to_disconnected()
+ self.loading = (self.damus?.pool.num_connecting ?? 0) != 0
}
}
@@ -292,7 +301,9 @@ struct ContentView: View {
pool.register_handler(sub_id: sub_id, handler: handle_event)
- self.pool = pool
+ self.damus = DamusState(pool: pool, pubkey: pubkey,
+ likes: EventCounter(our_pubkey: pubkey),
+ boosts: EventCounter(our_pubkey: pubkey))
pool.connect()
}
@@ -306,7 +317,28 @@ struct ContentView: View {
}
}
}
-
+
+ func handle_boost_event(_ ev: NostrEvent) {
+ damus!.boosts.add_event(ev)
+ }
+
+ 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!.likes.add_event(ev) {
+ case .user_already_liked:
+ break
+ case .success(let n):
+ let liked = Liked(like: 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
@@ -335,11 +367,15 @@ struct ContentView: View {
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 last_text_event = get_last_event_of_kind(relay_id: relay_id, kind: NostrKind.text.rawValue)
let since = get_since_time(last_event: last_text_event)
- var since_filter = NostrFilter.filter_text
+ var since_filter = NostrFilter.filter_kinds([1,5,6])
since_filter.since = since
+
+ let last_like_event = get_last_event_of_kind(relay_id: relay_id, kind: 7)
+ var like_filter = NostrFilter.filter_kinds([7])
+ like_filter.since = get_since_time(last_event: last_like_event)
+ //like_filter.ids = get_like_pow()
let last_metadata_event = get_last_event_of_kind(relay_id: relay_id, kind: NostrKind.metadata.rawValue)
var profile_filter = NostrFilter.filter_profiles
@@ -355,9 +391,9 @@ struct ContentView: View {
var contacts_filter = NostrFilter.filter_contacts
contacts_filter.authors = [self.pubkey]
- let filters = [since_filter, profile_filter, contacts_filter]
+ let filters = [since_filter, profile_filter, contacts_filter, like_filter]
print("connected to \(relay_id), refreshing from \(since)")
- self.pool?.send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: [relay_id])
+ self.damus?.pool.send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: [relay_id])
//self.pool?.send(.subscribe(.init(filters: [notification_filter], sub_id: "notifications")))
}
@@ -382,32 +418,42 @@ struct ContentView: View {
self.friend_events = self.friend_events.sorted { $0.created_at > $1.created_at }
}
+ func handle_text_event(_ ev: NostrEvent) {
+ if should_hide_event(ev) {
+ return
+ }
+
+ self.events.append(ev)
+ self.events = self.events.sorted { $0.created_at > $1.created_at }
+
+ 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 {
- 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 {
- if !should_hide_event(ev) {
- self.events.append(ev)
- self.events = self.events.sorted { $0.created_at > $1.created_at }
-
- handle_friend_event(ev)
-
- if is_notification(ev: ev, pubkey: pubkey) {
- handle_notification(ev: ev)
- }
- }
- } else if ev.kind == 0 {
- handle_metadata_event(ev)
- } else if ev.kind == 3 {
- handle_contact_event(ev)
-
- if ev.pubkey == pubkey {
- process_friend_events()
- }
+ 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 == 7 {
+ handle_like_event(ev)
+ } else if ev.kind == 3 {
+ handle_contact_event(ev)
+
+ if ev.pubkey == pubkey {
+ process_friend_events()
}
}
}
@@ -436,27 +482,29 @@ struct ContentView: View {
case .error(let merr):
let desc = merr.debugDescription
if desc.contains("Software caused connection abort") {
- self.pool?.reconnect(to: [relay_id])
+ self.damus?.pool.reconnect(to: [relay_id])
}
case .disconnected: fallthrough
case .cancelled:
- self.pool?.reconnect(to: [relay_id])
+ self.damus?.pool.reconnect(to: [relay_id])
case .reconnectSuggested(let t):
if t {
- self.pool?.reconnect(to: [relay_id])
+ self.damus?.pool.reconnect(to: [relay_id])
}
default:
break
}
- self.loading = (self.pool?.num_connecting ?? 0) != 0
+ self.loading = (self.damus?.pool.num_connecting ?? 0) != 0
print("ws_event \(ev)")
case .nostr_event(let ev):
switch ev {
case .event(let sub_id, let ev):
- if sub_id != self.sub_id {
+ // 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
}
@@ -593,3 +641,8 @@ func make_boost_event(_ ev: NostrEvent, privkey: String, pubkey: String) -> Nost
boost.sign(privkey: privkey)
return boost
}
+
+
+func get_like_pow() -> [String] {
+ return ["00000"] // 20 bits
+}
diff --git a/damus/Models/ActionBarModel.swift b/damus/Models/ActionBarModel.swift
@@ -9,14 +9,21 @@ import Foundation
class ActionBarModel: ObservableObject {
- @Published var our_like_event: NostrEvent? = nil
- @Published var our_boost_event: NostrEvent? = nil
+ @Published var our_like: NostrEvent?
+ @Published var our_boost: NostrEvent?
+ @Published var likes: Int
+
+ init(likes: Int, our_like: NostrEvent?, our_boost: NostrEvent?) {
+ self.likes = likes
+ self.our_like = our_like
+ self.our_boost = our_boost
+ }
var liked: Bool {
- return our_like_event != nil
+ return our_like != nil
}
var boosted: Bool {
- return our_boost_event != nil
+ return our_boost != nil
}
}
diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift
@@ -0,0 +1,16 @@
+//
+// DamusState.swift
+// damus
+//
+// Created by William Casarin on 2022-04-30.
+//
+
+import Foundation
+
+struct DamusState {
+ let pool: RelayPool
+ let pubkey: String
+ let likes: EventCounter
+ let boosts: EventCounter
+
+}
diff --git a/damus/Models/LikeCounter.swift b/damus/Models/LikeCounter.swift
@@ -0,0 +1,53 @@
+//
+// LikeCounter.swift
+// damus
+//
+// Created by William Casarin on 2022-04-30.
+//
+
+import Foundation
+
+
+class EventCounter {
+ var counts: [String: Int] = [:]
+ var user_events: [String: Set<String>] = [:]
+ var our_events: [String: NostrEvent] = [:]
+ var our_pubkey: String
+
+ enum LikeResult {
+ case user_already_liked
+ case success(Int)
+ }
+
+ init (our_pubkey: String) {
+ self.our_pubkey = our_pubkey
+ }
+
+ func add_event(_ ev: NostrEvent) -> LikeResult {
+ let pubkey = ev.pubkey
+
+ if self.user_events[pubkey] == nil {
+ self.user_events[pubkey] = Set()
+ }
+
+ if user_events[pubkey]!.contains(ev.id) {
+ // don't double count
+ return .user_already_liked
+ }
+
+ user_events[pubkey]!.insert(ev.id)
+
+ if ev.pubkey == self.our_pubkey {
+ our_events[ev.id] = ev
+ }
+
+ if counts[ev.id] == nil {
+ counts[ev.id] = 1
+ return .success(1)
+ }
+
+ counts[ev.id]! += 1
+
+ return .success(counts[ev.id]!)
+ }
+}
diff --git a/damus/Models/Liked.swift b/damus/Models/Liked.swift
@@ -0,0 +1,19 @@
+//
+// Liked.swift
+// damus
+//
+// Created by William Casarin on 2022-04-30.
+//
+
+import Foundation
+
+struct Liked {
+ let like: NostrEvent
+ let id: String
+ let total: Int
+}
+
+struct LikeRefs {
+ let thread_id: String?
+ let like_id: String
+}
diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift
@@ -0,0 +1,132 @@
+//
+// Mentions.swift
+// damus
+//
+// Created by William Casarin on 2022-05-04.
+//
+
+import Foundation
+
+enum MentionType {
+ case pubkey
+ case event
+}
+
+struct Mention {
+ let index: Int
+ let kind: MentionType
+}
+
+enum Block {
+ case text(String)
+ case mention(Mention)
+
+ var is_text: Bool {
+ if case .text = self {
+ return true
+ }
+ return false
+ }
+
+ var is_mention: Bool {
+ if case .mention = self {
+ return true
+ }
+ return false
+ }
+}
+
+struct ParsedMentions {
+ let blocks: [Block]
+}
+
+class Parser {
+ var pos: Int
+ var str: String
+
+ init(pos: Int, str: String) {
+ self.pos = pos
+ self.str = str
+ }
+}
+
+func consume_until(_ p: Parser, match: Character) -> Bool {
+ var i: Int = 0
+ let sub = substring(p.str, start: p.pos, end: p.str.count)
+ for c in sub {
+ if c == match {
+ p.pos += i
+ return true
+ }
+ i += 1
+ }
+
+ return false
+}
+
+func substring(_ s: String, start: Int, end: Int) -> Substring {
+ let ind = s.index(s.startIndex, offsetBy: start)
+ let end = s.index(s.startIndex, offsetBy: end)
+ return s[ind..<end]
+}
+
+func parse_textblock(str: String, from: Int, to: Int) -> Block {
+ return .text(String(substring(str, start: from, end: to)))
+}
+
+func parse_mentions(content: String, tags: [[String]]) -> [Block] {
+ let p = Parser(pos: 0, str: content)
+ var blocks: [Block] = []
+ var starting_from: Int = 0
+
+ while p.pos < content.count {
+ if (!consume_until(p, match: "#")) {
+ blocks.append(parse_textblock(str: p.str, from: starting_from, to: p.str.count))
+ return blocks
+ }
+
+ let pre_mention = p.pos
+ if let mention = parse_mention(p, tags: tags) {
+ blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention))
+ blocks.append(.mention(mention))
+ starting_from = p.pos
+ }
+ }
+
+ return blocks
+}
+
+func parse_mention(_ p: Parser, tags: [[String]]) -> Mention? {
+ let start = p.pos
+
+ if !parse_str(p, "#[") {
+ return nil
+ }
+
+ guard let digit = parse_digit(p) else {
+ p.pos = start
+ return nil
+ }
+
+ if !parse_char(p, "]") {
+ return nil
+ }
+
+ var kind: MentionType = .pubkey
+ if digit > tags.count - 1 {
+ return nil
+ }
+
+ if tags[digit].count == 0 {
+ return nil
+ }
+
+ switch tags[digit][0] {
+ case "e": kind = .event
+ case "p": kind = .pubkey
+ default: return nil
+ }
+
+ return Mention(index: digit, kind: kind)
+}
+
diff --git a/damus/Models/ParsedRefs.swift b/damus/Models/ParsedRefs.swift
@@ -0,0 +1,15 @@
+//
+// ParsedRefs.swift
+// damus
+//
+// Created by William Casarin on 2022-04-30.
+//
+
+import Foundation
+
+
+struct ReplyRefs {
+ let thread_id: String
+ let direct_reply: String
+}
+
diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift
@@ -10,34 +10,35 @@ import Foundation
class ProfileModel: ObservableObject {
@Published var events: [NostrEvent] = []
let pubkey: String
- let pool: RelayPool
+ let damus: DamusState
var seen_event: Set<String> = Set()
var sub_id = UUID().description
-
- init(pubkey: String, pool: RelayPool) {
+ init(pubkey: String, damus: DamusState) {
self.pubkey = pubkey
- self.pool = pool
+ self.damus = damus
}
func unsubscribe() {
print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)")
- pool.unsubscribe(sub_id: sub_id)
+ damus.pool.unsubscribe(sub_id: sub_id)
}
func subscribe() {
let kinds: [Int] = [
NostrKind.text.rawValue,
NostrKind.delete.rawValue,
+ NostrKind.contacts.rawValue,
+ NostrKind.metadata.rawValue,
NostrKind.boost.rawValue
]
- var filter = NostrFilter.filter_kinds(kinds)
- filter.authors = [pubkey]
-
+ var filter = NostrFilter.filter_authors([pubkey])
+ filter.kinds = kinds
+
print("subscribing to profile \(pubkey) with sub_id \(sub_id)")
- pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
+ damus.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
}
func add_event(_ ev: NostrEvent) {
diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift
@@ -62,14 +62,16 @@ class ThreadModel: ObservableObject {
}
func subscribe() {
- let kinds: [Int] = [1, 5, 6]
+ let kinds: [Int] = [1, 5, 6, 7]
var ref_events = NostrFilter.filter_kinds(kinds)
var events_filter = NostrFilter.filter_kinds(kinds)
+ //var likes_filter = NostrFilter.filter_kinds(7])
// TODO: add referenced relays
ref_events.referenced_ids = event.referenced_ids.map { $0.ref_id }
ref_events.referenced_ids!.append(event.id)
+ //likes_filter.ids = ref_events.referenced_ids!
events_filter.ids = ref_events.referenced_ids!
print("subscribing to thread \(event.id) with sub_id \(sub_id)")
@@ -110,7 +112,9 @@ class ThreadModel: ObservableObject {
switch res {
case .event(let sub_id, let ev):
if sub_id == self.sub_id {
- add_event(ev)
+ if ev.known_kind == .text {
+ add_event(ev)
+ }
}
case .notice(let note):
diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift
@@ -63,15 +63,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
}
private func get_referenced_ids(key: String) -> [ReferencedId] {
- return tags.reduce(into: []) { (acc, tag) in
- if tag.count >= 2 && tag[0] == key {
- var relay_id: String? = nil
- if tag.count >= 3 {
- relay_id = tag[2]
- }
- acc.append(ReferencedId(ref_id: tag[1], relay_id: relay_id, key: key))
- }
- }
+ return damus.get_referenced_ids(tags: self.tags, key: key)
}
public func is_root_event() -> Bool {
@@ -107,6 +99,23 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
return first
}
+ public func last_refid() -> ReferencedId? {
+ var mlast: Int? = nil
+ var i: Int = 0
+ for tag in tags {
+ if tag.count >= 2 && tag[0] == "e" {
+ mlast = i
+ }
+ i += 1
+ }
+
+ guard let last = mlast else {
+ return nil
+ }
+
+ return tag_to_refid(tags[last])
+ }
+
public func directly_references(_ id: String) -> Bool {
// conditions: if it only has 1 e ref
// OR it has more than 1 e ref, ignoring the first
@@ -152,14 +161,6 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
return false
}
- public func reply_ids() -> [ReferencedId] {
- var ids = self.referenced_ids.first.map { [$0] } ?? []
- ids.append(ReferencedId(ref_id: self.id, relay_id: nil, key: "e"))
- ids.append(contentsOf: self.referenced_pubkeys)
- ids.append(ReferencedId(ref_id: self.pubkey, relay_id: nil, key: "p"))
- return ids
- }
-
public var referenced_ids: [ReferencedId] {
return get_referenced_ids(key: "e")
}
@@ -353,3 +354,75 @@ func random_bytes(count: Int) -> Data {
}
return data
}
+
+func tag_to_refid(_ tag: [String]) -> ReferencedId? {
+ if tag.count == 0 {
+ return nil
+ }
+ if tag.count == 1 {
+ return nil
+ }
+
+ var relay_id: String? = nil
+ if tag.count > 2 {
+ relay_id = tag[2]
+ }
+
+ return ReferencedId(ref_id: tag[1], relay_id: relay_id, key: tag[0])
+}
+
+func parse_reply_refs(tags: [[String]]) -> ReplyRefs? {
+ let ids = get_referenced_ids(tags: tags, key: "e")
+
+ if ids.count == 0 {
+ return nil
+ }
+
+ let first = ids.first!
+ let last = ids.last!
+
+ return ReplyRefs(thread_id: first.ref_id, direct_reply: last.ref_id)
+}
+
+func get_referenced_ids(tags: [[String]], key: String) -> [ReferencedId] {
+ return tags.reduce(into: []) { (acc, tag) in
+ if tag.count >= 2 && tag[0] == key {
+ var relay_id: String? = nil
+ if tag.count >= 3 {
+ relay_id = tag[2]
+ }
+ acc.append(ReferencedId(ref_id: tag[1], relay_id: relay_id, key: key))
+ }
+ }
+}
+
+
+func make_like_event(pubkey: String, liked: NostrEvent) -> NostrEvent? {
+ var tags: [[String]]
+
+ if let refs = parse_reply_refs(tags: liked.tags) {
+ if refs.thread_id == refs.direct_reply {
+ tags = [["e", refs.thread_id], ["e", liked.id]]
+ } else {
+ tags = [["e", refs.thread_id], ["e", refs.direct_reply], ["e", liked.id]]
+ }
+ } else {
+ // root event
+ tags = [["e", liked.id]]
+ }
+
+
+ return NostrEvent(content: "", pubkey: pubkey, kind: 7, tags: tags)
+}
+
+func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
+ var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
+
+ ids.append(ReferencedId(ref_id: from.id, relay_id: nil, key: "e"))
+ ids.append(contentsOf: from.referenced_pubkeys.filter { $0.ref_id != our_pubkey })
+ if from.pubkey != our_pubkey {
+ ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
+ }
+ return ids
+}
+
diff --git a/damus/Nostr/NostrFilter.swift b/damus/Nostr/NostrFilter.swift
@@ -37,6 +37,10 @@ struct NostrFilter: Codable {
public static var filter_contacts: NostrFilter {
return filter_kinds([3])
}
+
+ public static func filter_authors(_ authors: [String]) -> NostrFilter {
+ return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: authors)
+ }
public static func filter_kinds(_ kinds: [Int]) -> NostrFilter {
return NostrFilter(ids: nil, kinds: kinds, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil)
diff --git a/damus/Notifications.swift b/damus/Notifications.swift
@@ -38,6 +38,12 @@ extension Notification.Name {
}
extension Notification.Name {
+ static var liked: Notification.Name {
+ return Notification.Name("liked")
+ }
+}
+
+extension Notification.Name {
static var click_profile_pic: Notification.Name {
return Notification.Name("click_profile_pic")
}
diff --git a/damus/Util/Parser.swift b/damus/Util/Parser.swift
@@ -0,0 +1,42 @@
+//
+// Parser.swift
+// damus
+//
+// Created by William Casarin on 2022-05-04.
+//
+
+import Foundation
+
+func parse_str(_ p: Parser, _ s: String) -> Bool {
+ let sub = substring(p.str, start: p.pos, end: p.pos + s.count)
+ if sub == s {
+ p.pos += s.count
+ return true
+ }
+ return false
+}
+
+func parse_char(_ p: Parser, _ c: Character) -> Bool{
+ let ind = p.str.index(p.str.startIndex, offsetBy: p.pos)
+
+ if p.str[ind] == c {
+ p.pos += 1
+ return true
+ }
+
+ return false
+}
+
+func parse_digit(_ p: Parser) -> Int? {
+ let ind = p.str.index(p.str.startIndex, offsetBy: p.pos)
+
+ if let c = p.str[ind].unicodeScalars.first {
+ let d = Int(c.value) - 48
+ if d >= 0 && d < 10 {
+ p.pos += 1
+ return Int(d)
+ }
+ }
+
+ return 0
+}
diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift
@@ -11,7 +11,10 @@ struct ChatView: View {
let event: NostrEvent
let prev_ev: NostrEvent?
let next_ev: NostrEvent?
-
+
+ let likes: EventCounter
+ let our_pubkey: String
+
@EnvironmentObject var profiles: Profiles
@EnvironmentObject var thread: ThreadModel
@@ -130,7 +133,7 @@ struct ChatView: View {
.textSelection(.enabled)
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
- EventActionBar(event: event)
+ EventActionBar(event: event, our_pubkey: our_pubkey, bar: make_actionbar_model(ev: event, counter: likes))
.environmentObject(profiles)
}
diff --git a/damus/Views/ChatroomView.swift b/damus/Views/ChatroomView.swift
@@ -10,6 +10,8 @@ import SwiftUI
struct ChatroomView: View {
@EnvironmentObject var thread: ThreadModel
@Environment(\.dismiss) var dismiss
+ let likes: EventCounter
+ let our_pubkey: String
var body: some View {
ScrollViewReader { scroller in
@@ -19,7 +21,9 @@ struct ChatroomView: View {
ForEach(Array(zip(thread.events, thread.events.indices)), id: \.0.id) { (ev, ind) in
ChatView(event: thread.events[ind],
prev_ev: ind > 0 ? thread.events[ind-1] : nil,
- next_ev: ind == count-1 ? nil : thread.events[ind+1]
+ next_ev: ind == count-1 ? nil : thread.events[ind+1],
+ likes: likes,
+ our_pubkey: our_pubkey
)
.onTapGesture {
if thread.event.id == ev.id {
diff --git a/damus/Views/EventActionBar.swift b/damus/Views/EventActionBar.swift
@@ -19,9 +19,10 @@ enum ActionBarSheet: Identifiable {
struct EventActionBar: View {
let event: NostrEvent
+ let our_pubkey: String
@State var sheet: ActionBarSheet? = nil
@EnvironmentObject var profiles: Profiles
- @StateObject var bar: ActionBarModel = ActionBarModel()
+ @StateObject var bar: ActionBarModel
var body: some View {
HStack {
@@ -38,25 +39,40 @@ struct EventActionBar: View {
}
.padding([.trailing], 40)
- EventActionButton(img: bar.liked ? "heart.fill" : "heart", col: bar.liked ? Color.red : nil) {
- if bar.liked {
- notify(.delete, bar.our_like_event)
- } else {
- notify(.like, event)
+ HStack(alignment: .bottom) {
+ Text("\(bar.likes > 0 ? "\(bar.likes)" : "")")
+ .font(.footnote)
+ .foregroundColor(Color.gray)
+
+ EventActionButton(img: bar.liked ? "heart.fill" : "heart", col: bar.liked ? Color.red : nil) {
+ if bar.liked {
+ notify(.delete, bar.our_like)
+ } else {
+ notify(.like, event)
+ }
}
}
.padding([.trailing], 40)
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
if bar.boosted {
- notify(.delete, bar.our_boost_event)
+ notify(.delete, bar.our_boost)
} else {
notify(.boost, event)
}
}
}
- .contentShape(Rectangle())
+ .onReceive(handle_notify(.liked)) { n in
+ let liked = n.object as! Liked
+ if liked.id != event.id {
+ return
+ }
+ self.bar.likes = liked.total
+ if liked.like.pubkey == our_pubkey {
+ self.bar.our_like = liked.like
+ }
+ }
}
}
@@ -67,6 +83,5 @@ func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) ->
.font(.footnote)
.foregroundColor(col == nil ? Color.gray : col!)
}
- .contentShape(Rectangle())
}
diff --git a/damus/Views/EventDetailView.swift b/damus/Views/EventDetailView.swift
@@ -32,7 +32,7 @@ enum CollapsedEvent: Identifiable {
struct EventDetailView: View {
let sub_id = UUID().description
- let pool: RelayPool
+ let damus: DamusState
@StateObject var thread: ThreadModel
@State var collapsed: Bool = true
@@ -70,7 +70,7 @@ struct EventDetailView: View {
toggle_thread_view()
}
case .event(let ev, let highlight):
- EventView(event: ev, highlight: highlight, has_action_bar: true, pool: pool)
+ EventView(event: ev, highlight: highlight, has_action_bar: true, damus: damus)
.onTapGesture {
if thread.event.id == ev.id {
toggle_thread_view()
diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift
@@ -40,7 +40,7 @@ struct EventView: View {
let event: NostrEvent
let highlight: Highlight
let has_action_bar: Bool
- let pool: RelayPool
+ let damus: DamusState
@EnvironmentObject var profiles: Profiles
@EnvironmentObject var action_bar: ActionBarModel
@@ -49,7 +49,7 @@ struct EventView: View {
let profile = profiles.lookup(id: event.pubkey)
HStack {
VStack {
- let pv = ProfileView(pool: pool, profile: ProfileModel(pubkey: event.pubkey, pool: pool))
+ let pv = ProfileView(damus: damus, profile: ProfileModel(pubkey: event.pubkey, damus: damus))
.environmentObject(profiles)
NavigationLink(destination: pv) {
@@ -81,7 +81,7 @@ struct EventView: View {
Spacer()
if has_action_bar {
- EventActionBar(event: event)
+ EventActionBar(event: event, our_pubkey: damus.pubkey, bar: make_actionbar_model(ev: event, counter: damus.likes))
.environmentObject(profiles)
}
@@ -152,3 +152,11 @@ func reply_others_desc(n: Int, n_pubkeys: Int) -> String {
}
+
+func make_actionbar_model(ev: NostrEvent, counter: EventCounter) -> ActionBarModel {
+ let likes = counter.counts[ev.id]
+ let our_like = counter.our_events[ev.id]
+ let our_boost: NostrEvent? = nil
+
+ return ActionBarModel(likes: likes ?? 0, our_like: our_like, our_boost: our_boost)
+}
diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift
@@ -24,14 +24,6 @@ struct NostrPost {
tag.append(relay_id)
}
new_ev.tags.append(tag)
- // filter our pubkeys
- new_ev.tags = new_ev.tags.filter {
- if $0[0] == "p" {
- return $0[1] != pubkey
- } else {
- return true
- }
- }
}
new_ev.calculate_id()
new_ev.sign(privkey: privkey)
diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift
@@ -13,7 +13,8 @@ enum ProfileTab: Hashable {
}
struct ProfileView: View {
- let pool: RelayPool
+ let damus: DamusState
+
@State private var selected_tab: ProfileTab = .posts
@StateObject var profile: ProfileModel
@@ -54,7 +55,7 @@ struct ProfileView: View {
Group {
switch(selected_tab) {
case .posts:
- TimelineView(events: $profile.events, pool: pool)
+ TimelineView(events: $profile.events, damus: damus)
.environmentObject(profiles)
case .following:
Text("Following")
diff --git a/damus/Views/ReplyView.swift b/damus/Views/ReplyView.swift
@@ -16,7 +16,7 @@ func all_referenced_pubkeys(_ ev: NostrEvent) -> [ReferencedId] {
struct ReplyView: View {
let replying_to: NostrEvent
- let pool: RelayPool
+ let damus: DamusState
@EnvironmentObject var profiles: Profiles
@@ -35,8 +35,8 @@ struct ReplyView: View {
.foregroundColor(.gray)
.font(.footnote)
}
- EventView(event: replying_to, highlight: .none, has_action_bar: false, pool: pool)
- PostView(references: replying_to.reply_ids())
+ EventView(event: replying_to, highlight: .none, has_action_bar: false, damus: damus)
+ PostView(references: gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to))
Spacer()
}
diff --git a/damus/Views/ThreadView.swift b/damus/Views/ThreadView.swift
@@ -11,7 +11,7 @@ import SwiftUI
struct ThreadView: View {
@State var is_chatroom: Bool = false
@StateObject var thread: ThreadModel
- let pool: RelayPool
+ let damus: DamusState
@EnvironmentObject var profiles: Profiles
@Environment(\.dismiss) var dismiss
@@ -19,12 +19,12 @@ struct ThreadView: View {
var body: some View {
Group {
if is_chatroom {
- ChatroomView()
+ ChatroomView(likes: damus.likes, our_pubkey: damus.pubkey)
.navigationBarTitle("Chat")
.environmentObject(profiles)
.environmentObject(thread)
} else {
- EventDetailView(pool: pool, thread: thread)
+ EventDetailView(damus: damus, thread: thread)
.navigationBarTitle("Thread")
.environmentObject(profiles)
.environmentObject(thread)
diff --git a/damus/Views/TimelineView.swift b/damus/Views/TimelineView.swift
@@ -17,7 +17,7 @@ struct TimelineView: View {
@EnvironmentObject var profiles: Profiles
- let pool: RelayPool
+ let damus: DamusState
var body: some View {
MainContent
@@ -37,11 +37,11 @@ struct TimelineView: View {
.environmentObject(profiles)
*/
- let tv = ThreadView(thread: ThreadModel(ev: ev, pool: pool), pool: pool)
+ let tv = ThreadView(thread: ThreadModel(ev: ev, pool: damus.pool), damus: damus)
.environmentObject(profiles)
NavigationLink(destination: tv) {
- EventView(event: ev, highlight: .none, has_action_bar: true, pool: pool)
+ EventView(event: ev, highlight: .none, has_action_bar: true, damus: damus)
}
.isDetailLink(true)
.buttonStyle(PlainButtonStyle())
diff --git a/damusTests/damusTests.swift b/damusTests/damusTests.swift
@@ -32,5 +32,15 @@ class damusTests: XCTestCase {
// Put the code you want to measure the time of here.
}
}
+
+ func testParseMention() throws {
+ let parsed = parse_mentions(content: "this is #[0] a mention", tags: [["e", "event_id"]])
+
+ XCTAssertNotNil(parsed)
+ XCTAssertEqual(parsed.count, 3)
+ XCTAssertTrue(parsed[0].is_text)
+ XCTAssertTrue(parsed[1].is_mention)
+ XCTAssertTrue(parsed[2].is_text)
+ }
}