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. 119
      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. 6
      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. 25
      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 */; }; 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; }; 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; };
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.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 */; }; 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */; };
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; }; 4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD5281D995700B3DE84 /* ActionBarModel.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 */; }; 4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; }; 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; };
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.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 */; }; 4C75EFB728049D990006080F /* RelayPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB628049D990006080F /* RelayPool.swift */; };
4C75EFB92804A2740006080F /* EventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB82804A2740006080F /* EventView.swift */; }; 4C75EFB92804A2740006080F /* EventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB82804A2740006080F /* EventView.swift */; };
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFBA2804A34C0006080F /* ProofOfWork.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 */; }; 4C8682872814DE470026224F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8682862814DE470026224F /* ProfileView.swift */; };
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; }; 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
@ -148,6 +160,11 @@
4C0A3F92280F66F5000448DE /* ReplyMap.swift */, 4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */, 4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */,
4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */, 4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */,
4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */,
4C3BEFDB281DCE6100B3DE84 /* Liked.swift */,
4C3BEFDD281DD59C00B3DE84 /* ParsedRefs.swift */,
4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */,
4C7FF7D42823313F009601DB /* Mentions.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
@ -192,6 +209,14 @@
path = Nostr; path = Nostr;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
4C363A8328233689006E126D /* Parser.swift */,
);
path = Util;
sourceTree = "<group>";
};
4CE6DEDA27F7A08100C66700 = { 4CE6DEDA27F7A08100C66700 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -216,6 +241,7 @@
4CE6DEE527F7A08100C66700 /* damus */ = { 4CE6DEE527F7A08100C66700 /* damus */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4C7FF7D628233637009601DB /* Util */,
4C0A3F8D280F63FF000448DE /* Models */, 4C0A3F8D280F63FF000448DE /* Models */,
4C75EFAB28049CC80006080F /* Nostr */, 4C75EFAB28049CC80006080F /* Nostr */,
4C75EFA72804823E0006080F /* Info.plist */, 4C75EFA72804823E0006080F /* Info.plist */,
@ -407,14 +433,19 @@
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */, 4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */, 4C75EFB92804A2740006080F /* EventView.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */, 4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */, 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */, 4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */, 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
4C363A8428233689006E126D /* Parser.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */, 4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */, 4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
4C3BEFDE281DD59C00B3DE84 /* ParsedRefs.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */, 4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */, 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */, 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
@ -429,6 +460,7 @@
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */, 4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */, 4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */,
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */, 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */, 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */, 4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */, 4C75EFA427FA577B0006080F /* PostView.swift in Sources */,

119
damus/ContentView.swift

@ -46,7 +46,7 @@ struct ContentView: View {
@State var profiles: Profiles = Profiles() @State var profiles: Profiles = Profiles()
@State var friends: [String: ()] = [:] @State var friends: [String: ()] = [:]
@State var loading: Bool = true @State var loading: Bool = true
@State var pool: RelayPool? = nil @State var damus: DamusState? = nil
@State var selected_timeline: Timeline? = .home @State var selected_timeline: Timeline? = .home
@State var is_thread_open: Bool = false @State var is_thread_open: Bool = false
@State var is_profile_open: Bool = false @State var is_profile_open: Bool = false
@ -135,15 +135,15 @@ struct ContentView: View {
var PostingTimelineView: some View { var PostingTimelineView: some View {
ZStack { ZStack {
if let pool = self.pool { if let damus = self.damus {
TimelineView(events: $friend_events, pool: pool) TimelineView(events: $friend_events, damus: damus)
.environmentObject(profiles) .environmentObject(profiles)
} }
PostButtonContainer PostButtonContainer
} }
} }
func MainContent(pool: RelayPool) -> some View { func MainContent(damus: DamusState) -> some View {
NavigationView { NavigationView {
VStack { VStack {
switch selected_timeline { switch selected_timeline {
@ -154,13 +154,13 @@ struct ContentView: View {
} }
case .notifications: case .notifications:
TimelineView(events: $notifications, pool: pool) TimelineView(events: $notifications, damus: damus)
.environmentObject(profiles) .environmentObject(profiles)
.navigationTitle("Notifications") .navigationTitle("Notifications")
case .global: case .global:
TimelineView(events: $events, pool: pool) TimelineView(events: $events, damus: damus)
.environmentObject(profiles) .environmentObject(profiles)
.navigationTitle("Global") .navigationTitle("Global")
case .none: case .none:
@ -174,9 +174,9 @@ struct ContentView: View {
var body: some View { var body: some View {
VStack { VStack {
if let pool = self.pool { if let damus = self.damus {
ZStack { ZStack {
MainContent(pool: pool) MainContent(damus: damus)
.padding([.bottom], -8.0) .padding([.bottom], -8.0)
LoadingContainer LoadingContainer
@ -193,14 +193,14 @@ struct ContentView: View {
case .post: case .post:
PostView(references: []) PostView(references: [])
case .reply(let event): case .reply(let event):
ReplyView(replying_to: event, pool: pool!) ReplyView(replying_to: event, damus: damus!)
.environmentObject(profiles) .environmentObject(profiles)
} }
} }
.onReceive(handle_notify(.boost)) { notif in .onReceive(handle_notify(.boost)) { notif in
let ev = notif.object as! NostrEvent let ev = notif.object as! NostrEvent
let boost = make_boost_event(ev, privkey: privkey, pubkey: pubkey) 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 .onReceive(handle_notify(.open_thread)) { obj in
//let ev = obj.object as! NostrEvent //let ev = obj.object as! NostrEvent
@ -211,9 +211,18 @@ struct ContentView: View {
let ev = notif.object as! NostrEvent let ev = notif.object as! NostrEvent
self.active_sheet = .reply(ev) 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 .onReceive(handle_notify(.broadcast_event)) { obj in
let ev = obj.object as! NostrEvent let ev = obj.object as! NostrEvent
self.pool?.send(.event(ev)) self.damus?.pool.send(.event(ev))
} }
.onReceive(handle_notify(.post)) { obj in .onReceive(handle_notify(.post)) { obj in
let post_res = obj.object as! NostrPostResult let post_res = obj.object as! NostrPostResult
@ -221,15 +230,15 @@ struct ContentView: View {
case .post(let post): case .post(let post):
print("post \(post.content)") print("post \(post.content)")
let new_ev = post.to_event(privkey: privkey, pubkey: pubkey) 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: case .cancel:
active_sheet = nil active_sheet = nil
print("post cancelled") print("post cancelled")
} }
} }
.onReceive(timer) { n in .onReceive(timer) { n in
self.pool?.connect_to_disconnected() self.damus?.pool.connect_to_disconnected()
self.loading = (self.pool?.num_connecting ?? 0) != 0 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) 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() 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) { func handle_metadata_event(_ ev: NostrEvent) {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else { guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
return return
@ -335,12 +367,16 @@ struct ContentView: View {
func send_filters(relay_id: String) { func send_filters(relay_id: String) {
// TODO: since times should be based on events from a specific relay // TODO: since times should be based on events from a specific relay
// perhaps we could mark this in the relay pool somehow // 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 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) 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 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) let last_metadata_event = get_last_event_of_kind(relay_id: relay_id, kind: NostrKind.metadata.rawValue)
var profile_filter = NostrFilter.filter_profiles var profile_filter = NostrFilter.filter_profiles
if let prof_since = get_metadata_since_time(last_metadata_event) { if let prof_since = get_metadata_since_time(last_metadata_event) {
@ -355,9 +391,9 @@ struct ContentView: View {
var contacts_filter = NostrFilter.filter_contacts var contacts_filter = NostrFilter.filter_contacts
contacts_filter.authors = [self.pubkey] 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)") 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"))) //self.pool?.send(.subscribe(.init(filters: [notification_filter], sub_id: "notifications")))
} }
@ -382,15 +418,11 @@ struct ContentView: View {
self.friend_events = self.friend_events.sorted { $0.created_at > $1.created_at } self.friend_events = self.friend_events.sorted { $0.created_at > $1.created_at }
} }
func process_event(relay_id: String, ev: NostrEvent) { func handle_text_event(_ ev: NostrEvent) {
if has_events[ev.id] == nil { if should_hide_event(ev) {
has_events[ev.id] = () return
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.append(ev)
self.events = self.events.sorted { $0.created_at > $1.created_at } self.events = self.events.sorted { $0.created_at > $1.created_at }
@ -400,8 +432,23 @@ struct ContentView: View {
handle_notification(ev: ev) 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 { } else if ev.kind == 0 {
handle_metadata_event(ev) handle_metadata_event(ev)
} else if ev.kind == 7 {
handle_like_event(ev)
} else if ev.kind == 3 { } else if ev.kind == 3 {
handle_contact_event(ev) handle_contact_event(ev)
@ -410,7 +457,6 @@ struct ContentView: View {
} }
} }
} }
}
func process_friend_events() { func process_friend_events() {
for event in events { for event in events {
@ -436,27 +482,29 @@ struct ContentView: View {
case .error(let merr): case .error(let merr):
let desc = merr.debugDescription let desc = merr.debugDescription
if desc.contains("Software caused connection abort") { if desc.contains("Software caused connection abort") {
self.pool?.reconnect(to: [relay_id]) self.damus?.pool.reconnect(to: [relay_id])
} }
case .disconnected: fallthrough case .disconnected: fallthrough
case .cancelled: case .cancelled:
self.pool?.reconnect(to: [relay_id]) self.damus?.pool.reconnect(to: [relay_id])
case .reconnectSuggested(let t): case .reconnectSuggested(let t):
if t { if t {
self.pool?.reconnect(to: [relay_id]) self.damus?.pool.reconnect(to: [relay_id])
} }
default: default:
break break
} }
self.loading = (self.pool?.num_connecting ?? 0) != 0 self.loading = (self.damus?.pool.num_connecting ?? 0) != 0
print("ws_event \(ev)") print("ws_event \(ev)")
case .nostr_event(let ev): case .nostr_event(let ev):
switch ev { switch ev {
case .event(let sub_id, let 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? // TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
return return
} }
@ -593,3 +641,8 @@ func make_boost_event(_ ev: NostrEvent, privkey: String, pubkey: String) -> Nost
boost.sign(privkey: privkey) boost.sign(privkey: privkey)
return boost 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 { class ActionBarModel: ObservableObject {
@Published var our_like_event: NostrEvent? = nil @Published var our_like: NostrEvent?
@Published var our_boost_event: NostrEvent? = nil @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 { var liked: Bool {
return our_like_event != nil return our_like != nil
} }
var boosted: Bool { 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 { class ProfileModel: ObservableObject {
@Published var events: [NostrEvent] = [] @Published var events: [NostrEvent] = []
let pubkey: String let pubkey: String
let pool: RelayPool let damus: DamusState
var seen_event: Set<String> = Set() var seen_event: Set<String> = Set()
var sub_id = UUID().description var sub_id = UUID().description
init(pubkey: String, damus: DamusState) {
init(pubkey: String, pool: RelayPool) {
self.pubkey = pubkey self.pubkey = pubkey
self.pool = pool self.damus = damus
} }
func unsubscribe() { func unsubscribe() {
print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)") 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() { func subscribe() {
let kinds: [Int] = [ let kinds: [Int] = [
NostrKind.text.rawValue, NostrKind.text.rawValue,
NostrKind.delete.rawValue, NostrKind.delete.rawValue,
NostrKind.contacts.rawValue,
NostrKind.metadata.rawValue,
NostrKind.boost.rawValue NostrKind.boost.rawValue
] ]
var filter = NostrFilter.filter_kinds(kinds) var filter = NostrFilter.filter_authors([pubkey])
filter.authors = [pubkey] filter.kinds = kinds
print("subscribing to profile \(pubkey) with sub_id \(sub_id)") 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) { func add_event(_ ev: NostrEvent) {

6
damus/Models/ThreadModel.swift

@ -62,14 +62,16 @@ class ThreadModel: ObservableObject {
} }
func subscribe() { func subscribe() {
let kinds: [Int] = [1, 5, 6] let kinds: [Int] = [1, 5, 6, 7]
var ref_events = NostrFilter.filter_kinds(kinds) var ref_events = NostrFilter.filter_kinds(kinds)
var events_filter = NostrFilter.filter_kinds(kinds) var events_filter = NostrFilter.filter_kinds(kinds)
//var likes_filter = NostrFilter.filter_kinds(7])
// TODO: add referenced relays // TODO: add referenced relays
ref_events.referenced_ids = event.referenced_ids.map { $0.ref_id } ref_events.referenced_ids = event.referenced_ids.map { $0.ref_id }
ref_events.referenced_ids!.append(event.id) ref_events.referenced_ids!.append(event.id)
//likes_filter.ids = ref_events.referenced_ids!
events_filter.ids = ref_events.referenced_ids! events_filter.ids = ref_events.referenced_ids!
print("subscribing to thread \(event.id) with sub_id \(sub_id)") print("subscribing to thread \(event.id) with sub_id \(sub_id)")
@ -110,8 +112,10 @@ class ThreadModel: ObservableObject {
switch res { switch res {
case .event(let sub_id, let ev): case .event(let sub_id, let ev):
if sub_id == self.sub_id { if sub_id == self.sub_id {
if ev.known_kind == .text {
add_event(ev) add_event(ev)
} }
}
case .notice(let note): case .notice(let note):
if note.contains("Too many subscription filters") { if note.contains("Too many subscription filters") {

107
damus/Nostr/NostrEvent.swift

@ -63,15 +63,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
} }
private func get_referenced_ids(key: String) -> [ReferencedId] { private func get_referenced_ids(key: String) -> [ReferencedId] {
return tags.reduce(into: []) { (acc, tag) in return damus.get_referenced_ids(tags: self.tags, key: key)
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))
}
}
} }
public func is_root_event() -> Bool { public func is_root_event() -> Bool {
@ -107,6 +99,23 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
return first 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 { public func directly_references(_ id: String) -> Bool {
// conditions: if it only has 1 e ref // conditions: if it only has 1 e ref
// OR it has more than 1 e ref, ignoring the first // OR it has more than 1 e ref, ignoring the first
@ -152,14 +161,6 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
return false 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] { public var referenced_ids: [ReferencedId] {
return get_referenced_ids(key: "e") return get_referenced_ids(key: "e")
} }
@ -353,3 +354,75 @@ func random_bytes(count: Int) -> Data {
} }
return 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]) 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 { 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) 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 { extension Notification.Name {
static var click_profile_pic: Notification.Name { static var click_profile_pic: Notification.Name {
return Notification.Name("click_profile_pic") 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 prev_ev: NostrEvent?
let next_ev: NostrEvent? let next_ev: NostrEvent?
let likes: EventCounter
let our_pubkey: String
@EnvironmentObject var profiles: Profiles @EnvironmentObject var profiles: Profiles
@EnvironmentObject var thread: ThreadModel @EnvironmentObject var thread: ThreadModel
@ -130,7 +133,7 @@ struct ChatView: View {
.textSelection(.enabled) .textSelection(.enabled)
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey { 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) .environmentObject(profiles)
} }

6
damus/Views/ChatroomView.swift

@ -10,6 +10,8 @@ import SwiftUI
struct ChatroomView: View { struct ChatroomView: View {
@EnvironmentObject var thread: ThreadModel @EnvironmentObject var thread: ThreadModel
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
let likes: EventCounter
let our_pubkey: String
var body: some View { var body: some View {
ScrollViewReader { scroller in ScrollViewReader { scroller in
@ -19,7 +21,9 @@ struct ChatroomView: View {
ForEach(Array(zip(thread.events, thread.events.indices)), id: \.0.id) { (ev, ind) in ForEach(Array(zip(thread.events, thread.events.indices)), id: \.0.id) { (ev, ind) in
ChatView(event: thread.events[ind], ChatView(event: thread.events[ind],
prev_ev: ind > 0 ? thread.events[ind-1] : nil, 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 { .onTapGesture {
if thread.event.id == ev.id { if thread.event.id == ev.id {

25
damus/Views/EventActionBar.swift

@ -19,9 +19,10 @@ enum ActionBarSheet: Identifiable {
struct EventActionBar: View { struct EventActionBar: View {
let event: NostrEvent let event: NostrEvent
let our_pubkey: String
@State var sheet: ActionBarSheet? = nil @State var sheet: ActionBarSheet? = nil
@EnvironmentObject var profiles: Profiles @EnvironmentObject var profiles: Profiles
@StateObject var bar: ActionBarModel = ActionBarModel() @StateObject var bar: ActionBarModel
var body: some View { var body: some View {
HStack { HStack {
@ -38,25 +39,40 @@ struct EventActionBar: View {
} }
.padding([.trailing], 40) .padding([.trailing], 40)
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) { EventActionButton(img: bar.liked ? "heart.fill" : "heart", col: bar.liked ? Color.red : nil) {
if bar.liked { if bar.liked {
notify(.delete, bar.our_like_event) notify(.delete, bar.our_like)
} else { } else {
notify(.like, event) notify(.like, event)
} }
} }
}
.padding([.trailing], 40) .padding([.trailing], 40)
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) { EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
if bar.boosted { if bar.boosted {
notify(.delete, bar.our_boost_event) notify(.delete, bar.our_boost)
} else { } else {
notify(.boost, event) 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) .font(.footnote)
.foregroundColor(col == nil ? Color.gray : col!) .foregroundColor(col == nil ? Color.gray : col!)
} }
.contentShape(Rectangle())
} }

4
damus/Views/EventDetailView.swift

@ -32,7 +32,7 @@ enum CollapsedEvent: Identifiable {
struct EventDetailView: View { struct EventDetailView: View {
let sub_id = UUID().description let sub_id = UUID().description
let pool: RelayPool let damus: DamusState
@StateObject var thread: ThreadModel @StateObject var thread: ThreadModel
@State var collapsed: Bool = true @State var collapsed: Bool = true
@ -70,7 +70,7 @@ struct EventDetailView: View {
toggle_thread_view() toggle_thread_view()
} }
case .event(let ev, let highlight): 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 { .onTapGesture {
if thread.event.id == ev.id { if thread.event.id == ev.id {
toggle_thread_view() toggle_thread_view()

14
damus/Views/EventView.swift

@ -40,7 +40,7 @@ struct EventView: View {
let event: NostrEvent let event: NostrEvent
let highlight: Highlight let highlight: Highlight
let has_action_bar: Bool let has_action_bar: Bool
let pool: RelayPool let damus: DamusState
@EnvironmentObject var profiles: Profiles @EnvironmentObject var profiles: Profiles
@EnvironmentObject var action_bar: ActionBarModel @EnvironmentObject var action_bar: ActionBarModel
@ -49,7 +49,7 @@ struct EventView: View {
let profile = profiles.lookup(id: event.pubkey) let profile = profiles.lookup(id: event.pubkey)
HStack { HStack {
VStack { 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) .environmentObject(profiles)
NavigationLink(destination: pv) { NavigationLink(destination: pv) {
@ -81,7 +81,7 @@ struct EventView: View {
Spacer() Spacer()
if has_action_bar { 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) .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) tag.append(relay_id)
} }
new_ev.tags.append(tag) 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.calculate_id()
new_ev.sign(privkey: privkey) new_ev.sign(privkey: privkey)

5
damus/Views/ProfileView.swift

@ -13,7 +13,8 @@ enum ProfileTab: Hashable {
} }
struct ProfileView: View { struct ProfileView: View {
let pool: RelayPool let damus: DamusState
@State private var selected_tab: ProfileTab = .posts @State private var selected_tab: ProfileTab = .posts
@StateObject var profile: ProfileModel @StateObject var profile: ProfileModel
@ -54,7 +55,7 @@ struct ProfileView: View {
Group { Group {
switch(selected_tab) { switch(selected_tab) {
case .posts: case .posts:
TimelineView(events: $profile.events, pool: pool) TimelineView(events: $profile.events, damus: damus)
.environmentObject(profiles) .environmentObject(profiles)
case .following: case .following:
Text("Following") Text("Following")

6
damus/Views/ReplyView.swift

@ -16,7 +16,7 @@ func all_referenced_pubkeys(_ ev: NostrEvent) -> [ReferencedId] {
struct ReplyView: View { struct ReplyView: View {
let replying_to: NostrEvent let replying_to: NostrEvent
let pool: RelayPool let damus: DamusState
@EnvironmentObject var profiles: Profiles @EnvironmentObject var profiles: Profiles
@ -35,8 +35,8 @@ struct ReplyView: View {
.foregroundColor(.gray) .foregroundColor(.gray)
.font(.footnote) .font(.footnote)
} }
EventView(event: replying_to, highlight: .none, has_action_bar: false, pool: pool) EventView(event: replying_to, highlight: .none, has_action_bar: false, damus: damus)
PostView(references: replying_to.reply_ids()) PostView(references: gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to))
Spacer() Spacer()
} }

6
damus/Views/ThreadView.swift

@ -11,7 +11,7 @@ import SwiftUI
struct ThreadView: View { struct ThreadView: View {
@State var is_chatroom: Bool = false @State var is_chatroom: Bool = false
@StateObject var thread: ThreadModel @StateObject var thread: ThreadModel
let pool: RelayPool let damus: DamusState
@EnvironmentObject var profiles: Profiles @EnvironmentObject var profiles: Profiles
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@ -19,12 +19,12 @@ struct ThreadView: View {
var body: some View { var body: some View {
Group { Group {
if is_chatroom { if is_chatroom {
ChatroomView() ChatroomView(likes: damus.likes, our_pubkey: damus.pubkey)
.navigationBarTitle("Chat") .navigationBarTitle("Chat")
.environmentObject(profiles) .environmentObject(profiles)
.environmentObject(thread) .environmentObject(thread)
} else { } else {
EventDetailView(pool: pool, thread: thread) EventDetailView(damus: damus, thread: thread)
.navigationBarTitle("Thread") .navigationBarTitle("Thread")
.environmentObject(profiles) .environmentObject(profiles)
.environmentObject(thread) .environmentObject(thread)

6
damus/Views/TimelineView.swift

@ -17,7 +17,7 @@ struct TimelineView: View {
@EnvironmentObject var profiles: Profiles @EnvironmentObject var profiles: Profiles
let pool: RelayPool let damus: DamusState
var body: some View { var body: some View {
MainContent MainContent
@ -37,11 +37,11 @@ struct TimelineView: View {
.environmentObject(profiles) .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) .environmentObject(profiles)
NavigationLink(destination: tv) { 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) .isDetailLink(true)
.buttonStyle(PlainButtonStyle()) .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