Browse Source

likes, mention parsing, lots of stuff

Signed-off-by: William Casarin <jb55@jb55.com>
profiles-everywhere
William Casarin 3 years ago
parent
commit
f42bc2e91e
  1. 32
      damus.xcodeproj/project.pbxproj
  2. 147
      damus/ContentView.swift
  3. 15
      damus/Models/ActionBarModel.swift
  4. 16
      damus/Models/DamusState.swift
  5. 53
      damus/Models/LikeCounter.swift
  6. 19
      damus/Models/Liked.swift
  7. 132
      damus/Models/Mentions.swift
  8. 15
      damus/Models/ParsedRefs.swift
  9. 17
      damus/Models/ProfileModel.swift
  10. 8
      damus/Models/ThreadModel.swift
  11. 107
      damus/Nostr/NostrEvent.swift
  12. 4
      damus/Nostr/NostrFilter.swift
  13. 6
      damus/Notifications.swift
  14. 42
      damus/Util/Parser.swift
  15. 5
      damus/Views/ChatView.swift
  16. 6
      damus/Views/ChatroomView.swift
  17. 33
      damus/Views/EventActionBar.swift
  18. 4
      damus/Views/EventDetailView.swift
  19. 14
      damus/Views/EventView.swift
  20. 8
      damus/Views/PostView.swift
  21. 5
      damus/Views/ProfileView.swift
  22. 6
      damus/Views/ReplyView.swift
  23. 6
      damus/Views/ThreadView.swift
  24. 6
      damus/Views/TimelineView.swift
  25. 10
      damusTests/damusTests.swift

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

147
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()
}
@ -307,6 +318,27 @@ 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,12 +367,16 @@ 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
if let prof_since = get_metadata_since_time(last_metadata_event) {
@ -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 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 }
func handle_text_event(_ ev: NostrEvent) {
if should_hide_event(ev) {
return
}
handle_friend_event(ev)
self.events.append(ev)
self.events = self.events.sorted { $0.created_at > $1.created_at }
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)
handle_friend_event(ev)
if ev.pubkey == pubkey {
process_friend_events()
}
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 {
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
}

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

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

53
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]!)
}
}

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

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

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

17
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) {

8
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):

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

4
damus/Nostr/NostrFilter.swift

@ -38,6 +38,10 @@ struct NostrFilter: Codable {
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)
}

6
damus/Notifications.swift

@ -37,6 +37,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")

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

5
damus/Views/ChatView.swift

@ -12,6 +12,9 @@ struct ChatView: View {
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)
}

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

33
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())
}

4
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()

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

8
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)

5
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")

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

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

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

10
damusTests/damusTests.swift

@ -33,4 +33,14 @@ class damusTests: XCTestCase {
}
}
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)
}
}

Loading…
Cancel
Save