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:
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)
}