diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index ec11b3df..9763044c 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -116,8 +116,12 @@ 4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */; }; 4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */; }; 4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838529656C8B00DC99E7 /* NIP05.swift */; }; + 4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */; }; 4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */; }; 4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838C296F710400DC99E7 /* Reposted.swift */; }; + 4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */; }; + 4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88392296F798300DC99E7 /* ReactionsModel.swift */; }; + 4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */; }; 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; }; 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; }; 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; }; @@ -310,8 +314,12 @@ 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedRelayView.swift; sourceTree = ""; }; 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelaysView.swift; sourceTree = ""; }; 4CB8838529656C8B00DC99E7 /* NIP05.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05.swift; sourceTree = ""; }; + 4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailBar.swift; sourceTree = ""; }; 4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05Badge.swift; sourceTree = ""; }; 4CB8838C296F710400DC99E7 /* Reposted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reposted.swift; sourceTree = ""; }; + 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsView.swift; sourceTree = ""; }; + 4CB88392296F798300DC99E7 /* ReactionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsModel.swift; sourceTree = ""; }; + 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; 4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = ""; }; 4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = ""; }; @@ -469,6 +477,7 @@ 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */, BA693073295D649800ADDB87 /* UserSettingsStore.swift */, 4FE60CDC295E1C5E00105A1F /* Wallet.swift */, + 4CB88392296F798300DC99E7 /* ReactionsModel.swift */, ); path = Models; sourceTree = ""; @@ -476,6 +485,8 @@ 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( + 4CB88394296F7F8100DC99E7 /* Reactions */, + 4CB88387296AF97C00DC99E7 /* ActionBar */, 4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */, 4C363A8728236948006E126D /* BlocksView.swift */, 4C285C8128385570008A31F1 /* CarouselView.swift */, @@ -488,7 +499,6 @@ 4C216F33286F5ACD00040376 /* DMView.swift */, E990020E2955F837003BBC5A /* EditMetadataView.swift */, 3169CAE4294E699400EE4006 /* Empty Views */, - 4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */, 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */, 4C75EFB82804A2740006080F /* EventView.swift */, 4C3AC79E2833115300E1F516 /* FollowButtonView.swift */, @@ -520,6 +530,7 @@ 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */, 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */, 647D9A8C2968520300A295DE /* SideMenuView.swift */, + 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */, ); path = Views; sourceTree = ""; @@ -565,6 +576,23 @@ path = Util; sourceTree = ""; }; + 4CB88387296AF97C00DC99E7 /* ActionBar */ = { + isa = PBXGroup; + children = ( + 4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */, + 4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */, + ); + path = ActionBar; + sourceTree = ""; + }; + 4CB88394296F7F8100DC99E7 /* Reactions */ = { + isa = PBXGroup; + children = ( + 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */, + ); + path = Reactions; + sourceTree = ""; + }; 4CE4F9DF285287A000C00DD9 /* Components */ = { isa = PBXGroup; children = ( @@ -840,6 +868,7 @@ 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */, 4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */, + 4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */, 4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */, 4C75EFB328049D640006080F /* NostrEvent.swift in Sources */, 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */, @@ -888,6 +917,8 @@ 4C3EA66528FF5F6800C48A62 /* mem.c in Sources */, 4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */, 4C3EA64F28FF59F200C48A62 /* tal.c in Sources */, + 4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */, + 4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */, 4C8682872814DE470026224F /* ProfileView.swift in Sources */, 4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */, 4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */, @@ -910,6 +941,7 @@ 4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */, 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */, 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */, + 4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */, 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */, 4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */, 4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift index b2651823..a7552dcc 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -216,7 +216,7 @@ struct ContentView: View { } } } - + var body: some View { VStack(alignment: .leading, spacing: 0) { if let damus = self.damus_state { @@ -229,9 +229,6 @@ struct ContentView: View { Button { isSideBarOpened.toggle() } label: { - let profile_model = ProfileModel(pubkey: damus_state!.pubkey, damus: damus_state!) - let followers_model = FollowersModel(damus_state: damus_state!, target: damus_state!.pubkey) - if let picture = damus_state?.profiles.lookup(id: pubkey)?.picture { ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles, picture: picture) } else { diff --git a/damus/Models/ActionBarModel.swift b/damus/Models/ActionBarModel.swift index e400911a..77cc47df 100644 --- a/damus/Models/ActionBarModel.swift +++ b/damus/Models/ActionBarModel.swift @@ -16,6 +16,10 @@ class ActionBarModel: ObservableObject { @Published var boosts: Int @Published var tips: Int64 + static func empty() -> ActionBarModel { + return ActionBarModel(likes: 0, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil) + } + init(likes: Int, boosts: Int, tips: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_tip: NostrEvent?) { self.likes = likes self.boosts = boosts diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift index b5b69a4a..8856e6cd 100644 --- a/damus/Models/FollowersModel.swift +++ b/damus/Models/FollowersModel.swift @@ -73,31 +73,30 @@ class FollowersModel: ObservableObject { } func handle_event(relay_id: String, ev: NostrConnectionEvent) { - switch ev { - case .ws_event: - break - case .nostr_event(let nev): - switch nev { - case .event(let sub_id, let ev): - guard sub_id == self.sub_id || sub_id == self.profiles_id else { - return - } - - if ev.known_kind == .contacts { - handle_contact_event(ev) - } else if ev.known_kind == .metadata { - process_metadata_event(profiles: damus_state.profiles, ev: ev) - } - - case .notice(let msg): - print("followingmodel notice: \(msg)") - - case .eose(let sub_id): - if sub_id == self.sub_id { - load_profiles(relay_id: relay_id) - } else if sub_id == self.profiles_id { - damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id]) - } + guard case .nostr_event(let nev) = ev else { + return + } + + switch nev { + case .event(let sub_id, let ev): + guard sub_id == self.sub_id || sub_id == self.profiles_id else { + return + } + + if ev.known_kind == .contacts { + handle_contact_event(ev) + } else if ev.known_kind == .metadata { + process_metadata_event(profiles: damus_state.profiles, ev: ev) + } + + case .notice(let msg): + print("followingmodel notice: \(msg)") + + case .eose(let sub_id): + if sub_id == self.sub_id { + load_profiles(relay_id: relay_id) + } else if sub_id == self.profiles_id { + damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id]) } } } diff --git a/damus/Models/ReactionsModel.swift b/damus/Models/ReactionsModel.swift new file mode 100644 index 00000000..92af5d7d --- /dev/null +++ b/damus/Models/ReactionsModel.swift @@ -0,0 +1,75 @@ +// +// LikesModel.swift +// damus +// +// Created by William Casarin on 2023-01-11. +// + +import Foundation + + +class ReactionsModel: ObservableObject { + let state: DamusState + let target: String + let sub_id: String + @Published var reactions: [NostrEvent] + + init (state: DamusState, target: String) { + self.state = state + self.target = target + self.sub_id = UUID().description + self.reactions = [] + } + + func get_filter() -> NostrFilter { + var filter = NostrFilter.filter_kinds([7]) + filter.referenced_ids = [target] + filter.limit = 500 + return filter + } + + func subscribe() { + let filter = get_filter() + let filters = [filter] + self.state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_nostr_event) + } + + func unsubscribe() { + self.state.pool.unsubscribe(sub_id: sub_id) + } + + func handle_event(relay_id: String, ev: NostrEvent) { + guard ev.kind == 7 else { + return + } + + guard let reacted_to = last_etag(tags: ev.tags) else { + return + } + + guard reacted_to == self.target else { + return + } + + if insert_uniq_sorted_event(events: &self.reactions, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) { + objectWillChange.send() + } + } + + func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) { + guard case .nostr_event(let nev) = ev else { + return + } + + switch nev { + case .event(_, let ev): + handle_event(relay_id: relay_id, ev: ev) + + case .notice(_): + break + case .eose(_): + load_profiles(profiles_subid: UUID().description, relay_id: relay_id, events: reactions, damus_state: state) + break + } + } +} diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift index 051633e7..95e46fe5 100644 --- a/damus/Nostr/NostrEvent.swift +++ b/damus/Nostr/NostrEvent.swift @@ -772,6 +772,15 @@ func validate_event(ev: NostrEvent) -> ValidationResult { return ok ? .ok : .bad_sig } +func last_etag(tags: [[String]]) -> String? { + var e: String? = nil + for tag in tags { + if tag.count >= 2 && tag[0] == "e" { + e = tag[1] + } + } + return e +} func inner_event_or_self(ev: NostrEvent) -> NostrEvent { guard let inner_ev = ev.inner_event else { diff --git a/damus/Views/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift similarity index 97% rename from damus/Views/EventActionBar.swift rename to damus/Views/ActionBar/EventActionBar.swift index 40827c9f..22dbd59d 100644 --- a/damus/Views/EventActionBar.swift +++ b/damus/Views/ActionBar/EventActionBar.swift @@ -29,14 +29,6 @@ struct EventActionBar: View { var body: some View { HStack { - /* - EventActionButton(img: "square.and.arrow.up") { - print("share") - } - - Spacer() - - */ if damus_state.keypair.privkey != nil { EventActionButton(img: "bubble.left", col: nil) { notify(.reply, event) diff --git a/damus/Views/ActionBar/EventDetailBar.swift b/damus/Views/ActionBar/EventDetailBar.swift new file mode 100644 index 00000000..9b76fa20 --- /dev/null +++ b/damus/Views/ActionBar/EventDetailBar.swift @@ -0,0 +1,39 @@ +// +// EventDetailBar.swift +// damus +// +// Created by William Casarin on 2023-01-08. +// + +import SwiftUI + +struct EventDetailBar: View { + let state: DamusState + let target: String + @StateObject var bar: ActionBarModel + + var body: some View { + HStack { + Text("\(bar.boosts)") + .font(.body.bold()) + Text("Reposts") + + NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) { + Text("\(bar.likes)") + .font(.body.bold()) + Text("Reactions") + } + .buttonStyle(PlainButtonStyle()) + + Text("\(bar.tips)") + .font(.body.bold()) + Text("Tips") + } + } +} + +struct EventDetailBar_Previews: PreviewProvider { + static var previews: some View { + EventDetailBar(state: test_damus_state(), target: "", bar: ActionBarModel.empty()) + } +} diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift index 0748f0c1..48568bfc 100644 --- a/damus/Views/EventView.swift +++ b/damus/Views/EventView.swift @@ -251,6 +251,13 @@ struct EventView: View { } let bar = make_actionbar_model(ev: event, damus: damus) + + if size == .selected { + EventDetailBar(state: damus, target: event.id, bar: bar) + Divider() + .padding([.bottom], 4) + } + EventActionBar(damus_state: damus, event: event, bar: bar) } diff --git a/damus/Views/Reactions/ReactionView.swift b/damus/Views/Reactions/ReactionView.swift new file mode 100644 index 00000000..32ab33c2 --- /dev/null +++ b/damus/Views/Reactions/ReactionView.swift @@ -0,0 +1,36 @@ +// +// ReactionView.swift +// damus +// +// Created by William Casarin on 2023-01-11. +// + +import SwiftUI + +struct ReactionView: View { + let damus_state: DamusState + let reaction: NostrEvent + + var content: String { + if reaction.content == "" || reaction.content == "+" { + return "❤️" + } + return reaction.content + } + + var body: some View { + HStack { + Text(content) + .font(Font.headline) + .frame(width: 50, height: 50) + + FollowUserView(target: .pubkey(reaction.pubkey), damus_state: damus_state) + } + } +} + +struct ReactionView_Previews: PreviewProvider { + static var previews: some View { + ReactionView(damus_state: test_damus_state(), reaction: NostrEvent(id: "", content: "🤙🏼", pubkey: "")) + } +} diff --git a/damus/Views/ReactionsView.swift b/damus/Views/ReactionsView.swift new file mode 100644 index 00000000..a4850fd1 --- /dev/null +++ b/damus/Views/ReactionsView.swift @@ -0,0 +1,38 @@ +// +// ReactionsView.swift +// damus +// +// Created by William Casarin on 2023-01-11. +// + +import SwiftUI + +struct ReactionsView: View { + let damus_state: DamusState + @StateObject var model: ReactionsModel + + var body: some View { + ScrollView { + LazyVStack { + ForEach(model.reactions, id: \.id) { ev in + ReactionView(damus_state: damus_state, reaction: ev) + } + } + .padding() + } + .navigationBarTitle("Reactions") + .onAppear { + model.subscribe() + } + .onDisappear { + model.unsubscribe() + } + } +} + +struct ReactionsView_Previews: PreviewProvider { + static var previews: some View { + let state = test_damus_state() + ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: "pubkey")) + } +} diff --git a/damus/Views/SearchView.swift b/damus/Views/SearchView.swift index 3fce9f69..d0f95b20 100644 --- a/damus/Views/SearchView.swift +++ b/damus/Views/SearchView.swift @@ -42,6 +42,7 @@ struct SearchView_Previews: PreviewProvider { let test_state = test_damus_state() let filter = NostrFilter.filter_hashtag(["bitcoin"]) let pool = test_state.pool + let model = SearchModel(pool: pool, search: filter) SearchView(appstate: test_state, search: model)