damus

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

commit 78c5b47f114f542e61b5322286fe5cb6716baa98
parent b100e9887bbd394a9d30137386baf0210d04d624
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 19 Apr 2022 19:46:30 -0700

chatroom

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

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 32++++++++++++++++++++++++++++++++
Adamus/Models/ReplyMap.swift | 19+++++++++++++++++++
Adamus/Models/ThreadModel.swift | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Nostr/NostrEvent.swift | 24++++++++++++++++++++++++
Adamus/Views/ChatView.swift | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/ChatroomView.swift | 45+++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/EventDetailView.swift | 134++++++++++++++++++++-----------------------------------------------------------
Mdamus/Views/EventView.swift | 5++---
Mdamus/Views/ProfilePicView.swift | 5++---
Adamus/Views/ReplyQuoteView.swift | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/ThreadView.swift | 44++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/TimelineView.swift | 10+++++++++-
12 files changed, 519 insertions(+), 107 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */; }; + 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; }; + 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; }; + 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; }; + 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; }; + 4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; }; 4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; }; 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; }; 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; }; @@ -57,6 +63,12 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomView.swift; sourceTree = "<group>"; }; + 4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; }; + 4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.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>"; }; + 4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; }; 4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; }; 4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; }; 4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; @@ -119,6 +131,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4C0A3F8D280F63FF000448DE /* Models */ = { + isa = PBXGroup; + children = ( + 4C0A3F8E280F640A000448DE /* ThreadModel.swift */, + 4C0A3F92280F66F5000448DE /* ReplyMap.swift */, + ); + path = Models; + sourceTree = "<group>"; + }; 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( @@ -132,6 +153,10 @@ 4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */, 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */, 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */, + 4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */, + 4C0A3F90280F6528000448DE /* ChatView.swift */, + 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */, + 4C0A3F96280F8E02000448DE /* ThreadView.swift */, ); path = Views; sourceTree = "<group>"; @@ -177,6 +202,7 @@ 4CE6DEE527F7A08100C66700 /* damus */ = { isa = PBXGroup; children = ( + 4C0A3F8D280F63FF000448DE /* Models */, 4C75EFAB28049CC80006080F /* Nostr */, 4C75EFA72804823E0006080F /* Info.plist */, 4C75EFA227FA576C0006080F /* Views */, @@ -365,12 +391,16 @@ 4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */, 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, 4C75EFB92804A2740006080F /* EventView.swift in Sources */, + 4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */, + 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */, 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, 4C75EFB328049D640006080F /* NostrEvent.swift in Sources */, 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */, 4C75EFB128049D510006080F /* NostrResponse.swift in Sources */, 4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */, 4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */, + 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */, + 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */, 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */, @@ -378,7 +408,9 @@ 4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */, 4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */, 4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */, + 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */, 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */, + 4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */, 4C75EFA427FA577B0006080F /* PostView.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */, 4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */, diff --git a/damus/Models/ReplyMap.swift b/damus/Models/ReplyMap.swift @@ -0,0 +1,19 @@ +// +// ReplyMap.swift +// damus +// +// Created by William Casarin on 2022-04-19. +// + +import Foundation + +class ReplyMap { + var replies: [String: String] = [:] + + func lookup(_ id: String) -> String? { + return replies[id] + } + func add(id: String, reply_id: String) { + replies[id] = reply_id + } +} diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -0,0 +1,93 @@ +// +// ThreadModel.swift +// damus +// +// Created by William Casarin on 2022-04-19. +// + +import Foundation + +/// manages the lifetime of a thread +class ThreadModel: ObservableObject { + @Published var event: NostrEvent + @Published var events: [NostrEvent] = [] + @Published var event_map: [String: Int] = [:] + var replies: ReplyMap = ReplyMap() + + let pool: RelayPool + let sub_id = UUID().description + + init(event: NostrEvent, pool: RelayPool) { + self.event = event + self.pool = pool + add_event(event) + } + + func unsubscribe() { + print("unsubscribing from thread \(event.id) with sub_id \(sub_id)") + self.pool.remove_handler(sub_id: sub_id) + self.pool.send(.unsubscribe(sub_id)) + } + + func subscribe() { + var ref_events = NostrFilter.filter_text + var events = NostrFilter.filter_text + + // TODO: add referenced relays + ref_events.referenced_ids = event.referenced_ids.map { $0.ref_id } + ref_events.referenced_ids!.append(event.id) + + events.ids = ref_events.referenced_ids! + + print("subscribing to thread \(event.id) with sub_id \(sub_id)") + pool.register_handler(sub_id: sub_id, handler: handle_event) + pool.send(.subscribe(.init(filters: [ref_events, events], sub_id: sub_id))) + } + + func lookup(_ event_id: String) -> NostrEvent? { + if let i = event_map[event_id] { + return events[i] + } + return nil + } + + func add_event(_ ev: NostrEvent) { + if event_map[ev.id] != nil { + return + } + + if let reply_id = ev.find_direct_reply() { + self.replies.add(id: ev.id, reply_id: reply_id) + } + + self.events.append(ev) + self.events = self.events.sorted { $0.created_at < $1.created_at } + var i: Int = 0 + for ev in events { + self.event_map[ev.id] = i + i += 1 + } + } + + func handle_event(relay_id: String, ev: NostrConnectionEvent) { + switch ev { + case .ws_event: + break + case .nostr_event(let res): + switch res { + case .event(let sub_id, let ev): + if sub_id == self.sub_id { + add_event(ev) + } + + case .notice(let note): + if note.contains("Too many subscription filters") { + // TODO: resend filters? + pool.reconnect(to: [relay_id]) + } + break + } + } + } + +} diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -79,6 +79,30 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible { return true } + /// find a non-root reply + public func find_direct_reply() -> String? { + var i = tags.count - 1 + var first: String? = nil + var matches: Int = 0 + + while i >= 0 { + let tag = tags[i] + if tag.count >= 2 && tag[0] == "e" { + if first == nil { + first = tag[1] + } + matches += 1 + } + i -= 1 + } + + if matches <= 1 { + return nil + } + + return first + } + public func directly_references(_ id: String) -> Bool { // conditions: if it only has 1 e ref // OR it has more than 1 e ref, ignoring the first diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift @@ -0,0 +1,151 @@ +// +// ChatView.swift +// damus +// +// Created by William Casarin on 2022-04-19. +// + +import SwiftUI + +struct ChatView: View { + let event: NostrEvent + let prev_ev: NostrEvent? + let next_ev: NostrEvent? + + @EnvironmentObject var profiles: Profiles + @EnvironmentObject var thread: ThreadModel + + var just_started: Bool { + return prev_ev == nil || prev_ev!.pubkey != event.pubkey + } + + var is_active: Bool { + thread.event.id == event.id + } + + func prev_reply_is_same() -> String? { + if let prev = prev_ev { + if let prev_reply_id = thread.replies.lookup(prev.id) { + if let cur_reply_id = thread.replies.lookup(event.id) { + if prev_reply_id != cur_reply_id { + return cur_reply_id + } + } + } + } + return nil + } + + func reply_is_new() -> String? { + guard let prev = self.prev_ev else { + // if they are both null they are the same? + return nil + } + + if thread.replies.lookup(prev.id) != thread.replies.lookup(event.id) { + return prev.id + } + + return nil + } + + var ReplyDescription: some View { + Text("\(reply_desc(profiles: profiles, event: event))") + .font(.footnote) + .foregroundColor(.gray) + .frame(maxWidth: .infinity, alignment: .leading) + } + + var body: some View { + let profile = profiles.lookup(id: event.pubkey) + HStack { + VStack { + if is_active || just_started { + ProfilePicView(picture: profile?.picture, size: 32, highlight: is_active ? .main : .none) + } + /* + if just_started { + ProfilePicView(picture: profile?.picture, size: 32, highlight: thread.event.id == event.id ? .main : .none) + } else { + Text("\(format_relative_time(event.created_at))") + .font(.footnote) + .foregroundColor(.gray.opacity(0.5)) + } + */ + + Spacer() + } + .frame(maxWidth: 32) + + VStack { + if just_started { + HStack { + ProfileName(pubkey: event.pubkey, profile: profile) + Text("\(format_relative_time(event.created_at))") + .foregroundColor(.gray) + Spacer() + } + } + + if let ref_id = thread.replies.lookup(event.id) { + ReplyQuoteView(quoter: event, event_id: ref_id) + .environmentObject(thread) + .environmentObject(profiles) + ReplyDescription + } + + Text(event.content) + .frame(maxWidth: .infinity, alignment: .leading) + .textSelection(.enabled) + + if next_ev == nil || next_ev!.pubkey != event.pubkey { + EventActionBar(event: event) + .environmentObject(profiles) + } + + Spacer() + } + .padding([.leading], 2) + //.border(Color.red) + } + .contentShape(Rectangle()) + .id(event.id) + .frame(minHeight: just_started ? PFP_SIZE : 0) + .padding([.bottom], next_ev == nil ? 4 : 0) + .onTapGesture { + if is_active { + convert_to_thread() + } else { + thread.event = event + } + } + //.border(Color.green) + + } + + @Environment(\.presentationMode) var presmode + + func dismiss() { + presmode.wrappedValue.dismiss() + } + + func convert_to_thread() { + NotificationCenter.default.post(name: .convert_to_thread, object: nil) + } +} + +extension Notification.Name { + static var convert_to_thread: Notification.Name { + return Notification.Name("convert_to_thread") + } +} + + +/* +struct ChatView_Previews: PreviewProvider { + static var previews: some View { + ChatView() + } +} + +*/ diff --git a/damus/Views/ChatroomView.swift b/damus/Views/ChatroomView.swift @@ -0,0 +1,45 @@ +// +// ChatroomView.swift +// damus +// +// Created by William Casarin on 2022-04-19. +// + +import SwiftUI + +struct ChatroomView: View { + @EnvironmentObject var thread: ThreadModel + + var body: some View { + ScrollViewReader { scroller in + ScrollView { + VStack { + let count = thread.events.count + ForEach(Array(zip(thread.events, thread.events.indices)), id: \.0.id) { (ev, ind) in + ChatView(event: thread.events[ind], + prev_ev: ind > 0 ? thread.events[ind-1] : nil, + next_ev: ind == count-1 ? nil : thread.events[ind+1] + ) + .environmentObject(thread) + } + } + } + .onAppear() { + scroll_to_event(scroller: scroller, id: thread.event.id, delay: 0.5, animate: true, anchor: .center) + } + } + } +} + + + + +/* +struct ChatroomView_Previews: PreviewProvider { + @State var events = [NostrEvent(content: "hello", pubkey: "pubkey")] + + static var previews: some View { + ChatroomView(events: events) + } +} + */ diff --git a/damus/Views/EventDetailView.swift b/damus/Views/EventDetailView.swift @@ -29,68 +29,16 @@ enum CollapsedEvent: Identifiable { } } + struct EventDetailView: View { - @State var event: NostrEvent let sub_id = UUID().description - - @State var events: [NostrEvent] = [] - @State var has_event: [String: ()] = [:] + + @StateObject var thread: ThreadModel @State var collapsed: Bool = true @EnvironmentObject var profiles: Profiles - - let pool: RelayPool - - func unsubscribe_to_thread() { - print("unsubscribing from thread \(event.id) with sub_id \(sub_id)") - self.pool.remove_handler(sub_id: sub_id) - self.pool.send(.unsubscribe(sub_id)) - } - - func subscribe_to_thread() { - var ref_events = NostrFilter.filter_text - var events = NostrFilter.filter_text - - // TODO: add referenced relays - ref_events.referenced_ids = event.referenced_ids.map { $0.ref_id } - ref_events.referenced_ids!.append(event.id) - - events.ids = ref_events.referenced_ids! - - print("subscribing to thread \(event.id) with sub_id \(sub_id)") - pool.register_handler(sub_id: sub_id, handler: handle_event) - pool.send(.subscribe(.init(filters: [ref_events, events], sub_id: sub_id))) - } - - func add_event(ev: NostrEvent) { - if sub_id != self.sub_id || self.has_event[ev.id] != nil { - return - } - self.add_event(ev) - } - - func handle_event(relay_id: String, ev: NostrConnectionEvent) { - switch ev { - case .ws_event: - break - case .nostr_event(let res): - switch res { - case .event(let sub_id, let ev): - if sub_id == self.sub_id { - add_event(ev: ev) - } - - case .notice(let note): - if note.contains("Too many subscription filters") { - // TODO: resend filters? - pool.reconnect(to: [relay_id]) - } - break - } - } - } - + func toggle_collapse_thread(scroller: ScrollViewProxy, id mid: String?, animate: Bool = true, anchor: UnitPoint = .center) { self.collapsed = !self.collapsed if let id = mid { @@ -100,21 +48,9 @@ struct EventDetailView: View { } } - func scroll_to_event(scroller: ScrollViewProxy, id: String, delay: Double, animate: Bool, anchor: UnitPoint = .center) { - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { - if animate { - withAnimation { - scroller.scrollTo(id, anchor: anchor) - } - } else { - scroller.scrollTo(id, anchor: anchor) - } - } - } - func OurEventView(proxy: ScrollViewProxy, ev: NostrEvent, highlight: Highlight, collapsed_events: [CollapsedEvent]) -> some View { Group { - if ev.id == event.id { + if ev.id == thread.event.id { EventView(event: ev, highlight: .main, has_action_bar: true) .onAppear() { scroll_to_event(scroller: proxy, id: ev.id, delay: 0.5, animate: true) @@ -134,7 +70,7 @@ struct EventDetailView: View { if !collapsed { toggle_collapse_thread(scroller: proxy, id: ev.id) } - self.event = ev + thread.event = ev } } } @@ -143,7 +79,7 @@ struct EventDetailView: View { func uncollapse_section(scroller: ScrollViewProxy, c: CollapsedEvents) { - let ev = events[c.start] + let ev = thread.events[c.start] print("uncollapsing section at \(c.start) '\(ev.content.prefix(12))...'") let start_id = ev.id @@ -153,40 +89,25 @@ struct EventDetailView: View { var body: some View { ScrollViewReader { proxy in ScrollView { - let collapsed_events = calculated_collapsed_events(collapsed: self.collapsed, active: self.event, events: self.events) - ForEach(collapsed_events, id: \.id) { cev in - switch cev { - case .collapsed(let c): - Text("··· \(c.count) other replies ···") - .font(.footnote) - .foregroundColor(.gray) - .onTapGesture { - self.uncollapse_section(scroller: proxy, c: c) - //self.toggle_collapse_thread(scroller: proxy, id: nil) - } - case .event(let ev, let highlight): - OurEventView(proxy: proxy, ev: ev, highlight: highlight, collapsed_events: collapsed_events) + let collapsed_events = calculated_collapsed_events(collapsed: self.collapsed, active: thread.event, events: thread.events) + ForEach(collapsed_events, id: \.id) { cev in + switch cev { + case .collapsed(let c): + Text("··· \(c.count) other replies ···") + .font(.footnote) + .foregroundColor(.gray) + .onTapGesture { + self.uncollapse_section(scroller: proxy, c: c) + //self.toggle_collapse_thread(scroller: proxy, id: nil) + } + case .event(let ev, let highlight): + OurEventView(proxy: proxy, ev: ev, highlight: highlight, collapsed_events: collapsed_events) + } } - } - } - .onDisappear() { - unsubscribe_to_thread() - } - .onAppear() { - self.add_event(event) - subscribe_to_thread() } } } - - func add_event(_ ev: NostrEvent) { - if self.has_event[ev.id] == nil { - self.has_event[ev.id] = () - self.events.append(ev) - self.events = self.events.sorted { $0.created_at < $1.created_at } - } - } } /* @@ -359,3 +280,16 @@ func any_collapsed(_ evs: [CollapsedEvent]) -> Bool { func print_event(_ ev: NostrEvent) { print(ev.description) } + +func scroll_to_event(scroller: ScrollViewProxy, id: String, delay: Double, animate: Bool, anchor: UnitPoint = .center) { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + if animate { + withAnimation { + scroller.scrollTo(id, anchor: anchor) + } + } else { + scroller.scrollTo(id, anchor: anchor) + } + } +} + diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -7,7 +7,6 @@ import Foundation import SwiftUI -import CachedAsyncImage enum Highlight { case none @@ -35,7 +34,7 @@ struct EventView: View { let has_action_bar: Bool @EnvironmentObject var profiles: Profiles - + var body: some View { let profile = profiles.lookup(id: event.pubkey) HStack { @@ -95,7 +94,7 @@ func format_relative_time(_ created_at: Int64) -> String func reply_desc(profiles: Profiles, event: NostrEvent) -> String { let (pubkeys, n) = event.reply_description if pubkeys.count == 0 { - return "Reply" + return "Reply to self" } let names: [String] = pubkeys.map { diff --git a/damus/Views/ProfilePicView.swift b/damus/Views/ProfilePicView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import CachedAsyncImage let PFP_SIZE: CGFloat? = 64 let CORNER_RADIUS: CGFloat = 32 @@ -42,13 +41,13 @@ struct ProfilePicView: View { } placeholder: { Color.purple.opacity(0.2) } - .frame(width: PFP_SIZE, height: PFP_SIZE) + .frame(width: size, height: size) .clipShape(Circle()) .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) .padding(2) } else { Color.purple.opacity(0.2) - .frame(width: PFP_SIZE, height: PFP_SIZE) + .frame(width: size, height: size) .cornerRadius(CORNER_RADIUS) .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) .padding(2) diff --git a/damus/Views/ReplyQuoteView.swift b/damus/Views/ReplyQuoteView.swift @@ -0,0 +1,64 @@ +// +// SwiftUIView.swift +// damus +// +// Created by William Casarin on 2022-04-19. +// + +import SwiftUI + +struct ReplyQuoteView: View { + let quoter: NostrEvent + let event_id: String + + @EnvironmentObject var profiles: Profiles + @EnvironmentObject var thread: ThreadModel + + func MainContent(event: NostrEvent) -> some View { + HStack(alignment: .top) { + ProfilePicView(picture: profiles.lookup(id: event.pubkey)?.picture, size: 16, highlight: .none) + //.border(Color.blue) + + VStack { + HStack { + ProfileName(pubkey: event.pubkey, profile: profiles.lookup(id: event.pubkey)) + Text("\(format_relative_time(event.created_at))") + .foregroundColor(.gray) + Spacer() + } + + Text(event.content) + .frame(maxWidth: .infinity, alignment: .leading) + .textSelection(.enabled) + + //Spacer() + } + //.border(Color.red) + } + //.border(Color.green) + } + + var body: some View { + Group { + if let event = thread.lookup(event_id) { + Group { + MainContent(event: event) + .padding(4) + } + .background(Color.secondary.opacity(0.2)) + .cornerRadius(8.0) + } else { + ProgressView() + .progressViewStyle(.circular) + } + } + } +} + +/* +struct SwiftUIView_Previews: PreviewProvider { + static var previews: some View { + SwiftUIView() + } +} + */ diff --git a/damus/Views/ThreadView.swift b/damus/Views/ThreadView.swift @@ -0,0 +1,44 @@ +// +// ThreadView.swift +// damus +// +// Created by William Casarin on 2022-04-19. +// + +import SwiftUI + +struct ThreadView: View { + @StateObject var thread: ThreadModel + @State var is_thread: Bool = false + + @EnvironmentObject var profiles: Profiles + + var body: some View { + Group { + ChatroomView() + .environmentObject(thread) + .onReceive(NotificationCenter.default.publisher(for: .convert_to_thread)) { _ in + is_thread = true + } + + let edv = EventDetailView(thread: thread).environmentObject(profiles) + NavigationLink(destination: edv, isActive: $is_thread) { + EmptyView() + } + } + .onDisappear() { + thread.unsubscribe() + } + .onAppear() { + thread.subscribe() + } + } +} + +/* +struct ThreadView_Previews: PreviewProvider { + static var previews: some View { + ThreadView() + } +} +*/ diff --git a/damus/Views/TimelineView.swift b/damus/Views/TimelineView.swift @@ -17,10 +17,18 @@ struct TimelineView: View { ScrollView { LazyVStack { ForEach(events, id: \.id) { (ev: NostrEvent) in - let evdet = EventDetailView(event: ev, pool: pool) + /* + let evdet = EventDetailView(thread: ThreadModel(event: ev, pool: pool)) .navigationBarTitle("Thread") .padding([.leading, .trailing], 6) .environmentObject(profiles) + */ + + let evdet = ThreadView(thread: ThreadModel(event: ev, pool: pool)) + .navigationBarTitle("Chat") + .padding([.leading, .trailing], 6) + .environmentObject(profiles) + NavigationLink(destination: evdet) { EventView(event: ev, highlight: .none, has_action_bar: true) }