damus

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

commit d950ad75b8eba277f79d0d05f81126913129b0f6
parent 41911908e75cc7e67bb44b704e993aa9d75f8752
Author: William Casarin <jb55@jb55.com>
Date:   Sun, 17 Apr 2022 08:05:45 -0700

better threads

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus/ContentView.swift | 4++--
Mdamus/Nostr/NostrEvent.swift | 22++++++++++++++++++++++
Mdamus/Views/EventActionBar.swift | 28+++++++++++++++++++++++++++-
Mdamus/Views/EventDetailView.swift | 52++++++++++++++++++++++++++++++++--------------------
Mdamus/Views/EventView.swift | 34++++++++++++++++++++++++++++++----
Mdamus/Views/ProfilePicView.swift | 55+++++++++++++++++++++++++++++++++++++++++++++++++++----
6 files changed, 164 insertions(+), 31 deletions(-)

diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -51,10 +51,10 @@ struct ContentView: View { ForEach(self.events, id: \.id) { (ev: NostrEvent) in if ev.is_local && timeline == .debug || (timeline == .global && !ev.is_local) || (timeline == .friends && is_friend(ev.pubkey)) { let evdet = EventDetailView(event: ev, pool: pool) - .navigationBarTitle("Note") + .navigationBarTitle("Thread") .environmentObject(profiles) NavigationLink(destination: evdet) { - EventView(event: ev, highlighted: false, has_action_bar: true) + EventView(event: ev, highlight: .none, has_action_bar: true) } .buttonStyle(PlainButtonStyle()) } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -64,6 +64,28 @@ class NostrEvent: Codable, Identifiable { } } + public func references(id: String, key: String) -> Bool { + for tag in tags { + if tag.count >= 2 && tag[0] == key { + if tag[1] == id { + return true + } + } + } + + return false + } + + public var is_reply: Bool { + for tag in tags { + if tag[0] == "e" { + return true + } + } + + return false + } + public var referenced_ids: [ReferencedId] { return get_referenced_ids(key: "e") } diff --git a/damus/Views/EventActionBar.swift b/damus/Views/EventActionBar.swift @@ -7,20 +7,46 @@ import SwiftUI +extension Notification.Name { + static var thread_focus: Notification.Name { + return Notification.Name("thread focus") + } +} + +enum ActionBarSheet: Identifiable { + case reply + + var id: String { + switch self { + case .reply: return "reply" + } + } +} struct EventActionBar: View { let event: NostrEvent + @State var sheet: ActionBarSheet? = nil + @EnvironmentObject var profiles: Profiles + var body: some View { HStack { EventActionButton(img: "bubble.left") { - print("reply") + self.sheet = .reply } + Spacer() + EventActionButton(img: "square.and.arrow.up") { print("share") } } + .sheet(item: $sheet) { sheet in + switch sheet { + case .reply: + ReplyView(replying_to: event) + } + } } } diff --git a/damus/Views/EventDetailView.swift b/damus/Views/EventDetailView.swift @@ -8,7 +8,7 @@ import SwiftUI struct EventDetailView: View { - let event: NostrEvent + @State var event: NostrEvent let sub_id = UUID().description @@ -21,8 +21,8 @@ struct EventDetailView: View { func unsubscribe_to_thread() { print("unsubscribing from thread \(event.id) with sub_id \(sub_id)") - self.pool.send(.unsubscribe(sub_id)) self.pool.remove_handler(sub_id: sub_id) + self.pool.send(.unsubscribe(sub_id)) } func subscribe_to_thread() { @@ -53,8 +53,11 @@ struct EventDetailView: View { } self.add_event(ev) - case .notice(_): - // TODO: handle notices in threads? + case .notice(let note): + if note.contains("Too many subscription filters") { + // TODO: resend filters? + pool.reconnect(to: [relay_id]) + } break } } @@ -64,25 +67,24 @@ struct EventDetailView: View { ScrollViewReader { proxy in ScrollView { ForEach(events, id: \.id) { ev in - let is_active_id = ev.id == event.id - if is_active_id { - EventView(event: ev, highlighted: is_active_id, has_action_bar: true) - .onAppear() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - withAnimation { - proxy.scrollTo(event.id) + Group { + let is_active_id = ev.id == event.id + if is_active_id { + EventView(event: ev, highlight: .main, has_action_bar: true) + .onAppear() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + withAnimation { + proxy.scrollTo(event.id) + } } } - } - } else { - let evdet = EventDetailView(event: ev, pool: pool) - .navigationBarTitle("Note") - .environmentObject(profiles) - - NavigationLink(destination: evdet) { - EventView(event: ev, highlighted: is_active_id, has_action_bar: true) + } else { + let highlight = determine_highlight(current: ev, active: event) + EventView(event: ev, highlight: highlight, has_action_bar: true) + .onTapGesture { + self.event = ev + } } - .buttonStyle(PlainButtonStyle()) } } } @@ -115,3 +117,13 @@ struct EventDetailView_Previews: PreviewProvider { } } */ + +func determine_highlight(current: NostrEvent, active: NostrEvent) -> Highlight +{ + if active.references(id: current.id, key: "e") { + return .replied_to(active.id) + } else if current.references(id: active.id, key: "e") { + return .replied_to(current.id) + } + return .none +} diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -9,18 +9,39 @@ import Foundation import SwiftUI import CachedAsyncImage +enum Highlight { + case none + case main + case referenced(String) + case replied_to(String) + + var is_none: Bool { + switch self { + case .none: return true + default: return false + } + } + + var is_replied_to: Bool { + switch self { + case .replied_to: return true + default: return false + } + } +} + struct EventView: View { let event: NostrEvent - let highlighted: Bool + let highlight: Highlight let has_action_bar: Bool - + @EnvironmentObject var profiles: Profiles var body: some View { let profile = profiles.lookup(id: event.pubkey) HStack { VStack { - ProfilePicView(picture: profile?.picture, size: 64, highlighted: highlighted) + ProfilePicView(picture: profile?.picture, size: 64, highlight: highlight) Spacer() } @@ -30,12 +51,17 @@ struct EventView: View { ProfileName(pubkey: event.pubkey, profile: profile) Text("\(format_relative_time(event.created_at))") .foregroundColor(.gray) + if event.is_reply { + Label("", systemImage: "arrowshape.turn.up.left") + .font(.footnote) + .foregroundColor(.gray) + } Spacer() if (event.pow ?? 0) >= 10 { PowView(event.pow) } } - + Text(event.content) .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) diff --git a/damus/Views/ProfilePicView.swift b/damus/Views/ProfilePicView.swift @@ -11,10 +11,30 @@ import CachedAsyncImage let PFP_SIZE: CGFloat? = 64 let CORNER_RADIUS: CGFloat = 32 +func id_to_color(_ id: String) -> Color { + return .init(hex: String(id.reversed().prefix(6))) +} + +func highlight_color(_ h: Highlight) -> Color { + switch h { + case .none: return Color.black + case .main: return Color.red + case .referenced(let id): return Color.blue + case .replied_to: return Color.blue + } +} + +func pfp_line_width(_ h: Highlight) -> CGFloat { + if h.is_none { + return 0 + } + return 4 +} + struct ProfilePicView: View { let picture: String? let size: CGFloat - let highlighted: Bool + let highlight: Highlight var body: some View { if let pic = picture.flatMap({ URL(string: $0) }) { @@ -25,13 +45,13 @@ struct ProfilePicView: View { } .frame(width: PFP_SIZE, height: PFP_SIZE) .clipShape(Circle()) - .overlay(Circle().stroke(highlighted ? Color.red : Color.black, lineWidth: highlighted ? 4 : 0)) + .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) .padding(2) } else { Color.purple.opacity(0.1) .frame(width: PFP_SIZE, height: PFP_SIZE) .cornerRadius(CORNER_RADIUS) - .overlay(Circle().stroke(highlighted ? Color.red : Color.black, lineWidth: highlighted ? 4 : 0)) + .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) .padding(2) } } @@ -39,6 +59,33 @@ struct ProfilePicView: View { struct ProfilePicView_Previews: PreviewProvider { static var previews: some View { - ProfilePicView(picture: "http://cdn.jb55.com/img/red-me.jpg", size: 64, highlighted: false) + ProfilePicView(picture: "http://cdn.jb55.com/img/red-me.jpg", size: 64, highlight: .none) + } +} + + +extension Color { + init(hex: String) { + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (1, 1, 1, 0) + } + + self.init( + .sRGB, + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255, + opacity: Double(a) / 255 + ) } }