damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit b2b790a969560971fd99f524d2749d75ee27a861
parent 907f0d236f93eae27b896ed8459806969bf9ca51
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 10 Jan 2023 08:12:04 -0800

Reactions View

Changelog-Added: Added Reactions View

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 34+++++++++++++++++++++++++++++++++-
Mdamus/ContentView.swift | 5+----
Mdamus/Models/ActionBarModel.swift | 4++++
Mdamus/Models/FollowersModel.swift | 49++++++++++++++++++++++++-------------------------
Adamus/Models/ReactionsModel.swift | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Nostr/NostrEvent.swift | 9+++++++++
Adamus/Views/ActionBar/EventActionBar.swift | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/ActionBar/EventDetailBar.swift | 39+++++++++++++++++++++++++++++++++++++++
Ddamus/Views/EventActionBar.swift | 191-------------------------------------------------------------------------------
Mdamus/Views/EventView.swift | 7+++++++
Adamus/Views/Reactions/ReactionView.swift | 36++++++++++++++++++++++++++++++++++++
Adamus/Views/ReactionsView.swift | 38++++++++++++++++++++++++++++++++++++++
Mdamus/Views/SearchView.swift | 1+
13 files changed, 450 insertions(+), 221 deletions(-)

diff --git 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 = "<group>"; }; 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelaysView.swift; sourceTree = "<group>"; }; 4CB8838529656C8B00DC99E7 /* NIP05.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05.swift; sourceTree = "<group>"; }; + 4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailBar.swift; sourceTree = "<group>"; }; 4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05Badge.swift; sourceTree = "<group>"; }; 4CB8838C296F710400DC99E7 /* Reposted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reposted.swift; sourceTree = "<group>"; }; + 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsView.swift; sourceTree = "<group>"; }; + 4CB88392296F798300DC99E7 /* ReactionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsModel.swift; sourceTree = "<group>"; }; + 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; }; 4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; }; 4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; }; 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; }; @@ -469,6 +477,7 @@ 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */, BA693073295D649800ADDB87 /* UserSettingsStore.swift */, 4FE60CDC295E1C5E00105A1F /* Wallet.swift */, + 4CB88392296F798300DC99E7 /* ReactionsModel.swift */, ); path = Models; sourceTree = "<group>"; @@ -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 = "<group>"; @@ -565,6 +576,23 @@ path = Util; sourceTree = "<group>"; }; + 4CB88387296AF97C00DC99E7 /* ActionBar */ = { + isa = PBXGroup; + children = ( + 4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */, + 4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */, + ); + path = ActionBar; + sourceTree = "<group>"; + }; + 4CB88394296F7F8100DC99E7 /* Reactions */ = { + isa = PBXGroup; + children = ( + 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */, + ); + path = Reactions; + sourceTree = "<group>"; + }; 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift @@ -0,0 +1,183 @@ +// +// EventActionBar.swift +// damus +// +// Created by William Casarin on 2022-04-16. +// + +import SwiftUI +import UIKit + +enum ActionBarSheet: Identifiable { + case reply + + var id: String { + switch self { + case .reply: return "reply" + } + } +} + +struct EventActionBar: View { + let damus_state: DamusState + let event: NostrEvent + let generator = UIImpactFeedbackGenerator(style: .medium) + @State var sheet: ActionBarSheet? = nil + @State var confirm_boost: Bool = false + @State var show_share_sheet: Bool = false + @StateObject var bar: ActionBarModel + + var body: some View { + HStack { + if damus_state.keypair.privkey != nil { + EventActionButton(img: "bubble.left", col: nil) { + notify(.reply, event) + } + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + + HStack(alignment: .bottom) { + Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")") + .font(.footnote.weight(.medium)) + .foregroundColor(bar.boosted ? Color.green : Color.gray) + + EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) { + if bar.boosted { + notify(.delete, bar.our_boost) + } else { + self.confirm_boost = true + } + } + } + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + + HStack(alignment: .bottom) { + Text("\(bar.likes > 0 ? "\(bar.likes)" : "")") + .font(.footnote.weight(.medium)) + .foregroundColor(bar.liked ? Color.orange : Color.gray) + + LikeButton(liked: bar.liked) { + if bar.liked { + notify(.delete, bar.our_like) + } else { + send_like() + } + } + } + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + + EventActionButton(img: "square.and.arrow.up", col: Color.gray) { + show_share_sheet = true + } + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + + /* + HStack(alignment: .bottom) { + Text("\(bar.tips > 0 ? "\(bar.tips)" : "")") + .font(.footnote) + .foregroundColor(bar.tipped ? Color.orange : Color.gray) + + EventActionButton(img: bar.tipped ? "bitcoinsign.circle.fill" : "bitcoinsign.circle", col: bar.tipped ? Color.orange : nil) { + if bar.tipped { + //notify(.delete, bar.our_tip) + } else { + //notify(.boost, event) + } + } + } + */ + } + .sheet(isPresented: $show_share_sheet) { + if let note_id = bech32_note_id(event.id) { + if let url = URL(string: "https://damus.io/" + note_id) { + ShareSheet(activityItems: [url]) + } + } + } + .alert(NSLocalizedString("Boost", comment: "Title of alert for confirming to boost a post."), isPresented: $confirm_boost) { + Button("Cancel") { + confirm_boost = false + } + Button(NSLocalizedString("Boost", comment: "Button to confirm boosting a post.")) { + send_boost() + } + } message: { + Text("Are you sure you want to boost this post?", comment: "Alert message to ask if user wants to boost a post.") + } + .onReceive(handle_notify(.liked)) { n in + let liked = n.object as! Counted + if liked.id != event.id { + return + } + self.bar.likes = liked.total + if liked.event.pubkey == damus_state.keypair.pubkey { + self.bar.our_like = liked.event + } + } + } + + func send_boost() { + guard let privkey = self.damus_state.keypair.privkey else { + return + } + + let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: self.event) + + self.bar.our_boost = boost + + damus_state.pool.send(.event(boost)) + } + + func send_like() { + guard let privkey = damus_state.keypair.privkey else { + return + } + + let like_ev = make_like_event(pubkey: damus_state.pubkey, privkey: privkey, liked: event) + + self.bar.our_like = like_ev + + generator.impactOccurred() + + damus_state.pool.send(.event(like_ev)) + } +} + + +func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View { + Button(action: action) { + Label("&nbsp;", systemImage: img) + .font(.footnote.weight(.medium)) + .foregroundColor(col == nil ? Color.gray : col!) + } +} + +struct LikeButton: View { + let liked: Bool + let action: () -> () + + @Environment(\.colorScheme) var colorScheme + + var body: some View { + Button(action: action) { + if liked { + Text("🤙", comment: "Button with emoji to like an event.") + } else { + Image("shaka") + .renderingMode(.template) + .foregroundColor(.gray) + } + } + } +} + + +struct EventActionBar_Previews: PreviewProvider { + static var previews: some View { + let pk = "pubkey" + let ds = test_damus_state() + let bar = ActionBarModel(likes: 0, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil) + let ev = NostrEvent(content: "hi", pubkey: pk) + EventActionBar(damus_state: ds, event: ev, bar: bar) + } +} diff --git a/damus/Views/ActionBar/EventDetailBar.swift 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/EventActionBar.swift b/damus/Views/EventActionBar.swift @@ -1,191 +0,0 @@ -// -// EventActionBar.swift -// damus -// -// Created by William Casarin on 2022-04-16. -// - -import SwiftUI -import UIKit - -enum ActionBarSheet: Identifiable { - case reply - - var id: String { - switch self { - case .reply: return "reply" - } - } -} - -struct EventActionBar: View { - let damus_state: DamusState - let event: NostrEvent - let generator = UIImpactFeedbackGenerator(style: .medium) - @State var sheet: ActionBarSheet? = nil - @State var confirm_boost: Bool = false - @State var show_share_sheet: Bool = false - @StateObject var bar: ActionBarModel - - 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) - } - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - } - - HStack(alignment: .bottom) { - Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")") - .font(.footnote.weight(.medium)) - .foregroundColor(bar.boosted ? Color.green : Color.gray) - - EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) { - if bar.boosted { - notify(.delete, bar.our_boost) - } else { - self.confirm_boost = true - } - } - } - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - - HStack(alignment: .bottom) { - Text("\(bar.likes > 0 ? "\(bar.likes)" : "")") - .font(.footnote.weight(.medium)) - .foregroundColor(bar.liked ? Color.orange : Color.gray) - - LikeButton(liked: bar.liked) { - if bar.liked { - notify(.delete, bar.our_like) - } else { - send_like() - } - } - } - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - - EventActionButton(img: "square.and.arrow.up", col: Color.gray) { - show_share_sheet = true - } - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - - /* - HStack(alignment: .bottom) { - Text("\(bar.tips > 0 ? "\(bar.tips)" : "")") - .font(.footnote) - .foregroundColor(bar.tipped ? Color.orange : Color.gray) - - EventActionButton(img: bar.tipped ? "bitcoinsign.circle.fill" : "bitcoinsign.circle", col: bar.tipped ? Color.orange : nil) { - if bar.tipped { - //notify(.delete, bar.our_tip) - } else { - //notify(.boost, event) - } - } - } - */ - } - .sheet(isPresented: $show_share_sheet) { - if let note_id = bech32_note_id(event.id) { - if let url = URL(string: "https://damus.io/" + note_id) { - ShareSheet(activityItems: [url]) - } - } - } - .alert(NSLocalizedString("Boost", comment: "Title of alert for confirming to boost a post."), isPresented: $confirm_boost) { - Button("Cancel") { - confirm_boost = false - } - Button(NSLocalizedString("Boost", comment: "Button to confirm boosting a post.")) { - send_boost() - } - } message: { - Text("Are you sure you want to boost this post?", comment: "Alert message to ask if user wants to boost a post.") - } - .onReceive(handle_notify(.liked)) { n in - let liked = n.object as! Counted - if liked.id != event.id { - return - } - self.bar.likes = liked.total - if liked.event.pubkey == damus_state.keypair.pubkey { - self.bar.our_like = liked.event - } - } - } - - func send_boost() { - guard let privkey = self.damus_state.keypair.privkey else { - return - } - - let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: self.event) - - self.bar.our_boost = boost - - damus_state.pool.send(.event(boost)) - } - - func send_like() { - guard let privkey = damus_state.keypair.privkey else { - return - } - - let like_ev = make_like_event(pubkey: damus_state.pubkey, privkey: privkey, liked: event) - - self.bar.our_like = like_ev - - generator.impactOccurred() - - damus_state.pool.send(.event(like_ev)) - } -} - - -func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View { - Button(action: action) { - Label("&nbsp;", systemImage: img) - .font(.footnote.weight(.medium)) - .foregroundColor(col == nil ? Color.gray : col!) - } -} - -struct LikeButton: View { - let liked: Bool - let action: () -> () - - @Environment(\.colorScheme) var colorScheme - - var body: some View { - Button(action: action) { - if liked { - Text("🤙", comment: "Button with emoji to like an event.") - } else { - Image("shaka") - .renderingMode(.template) - .foregroundColor(.gray) - } - } - } -} - - -struct EventActionBar_Previews: PreviewProvider { - static var previews: some View { - let pk = "pubkey" - let ds = test_damus_state() - let bar = ActionBarModel(likes: 0, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil) - let ev = NostrEvent(content: "hi", pubkey: pk) - EventActionBar(damus_state: ds, event: ev, bar: bar) - } -} diff --git 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 @@ -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 @@ -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 @@ -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)