Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2022-04-19 19:46:30 -07:00
parent b100e9887b
commit 78c5b47f11
12 changed files with 519 additions and 107 deletions

151
damus/Views/ChatView.swift Normal file
View File

@@ -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()
}
}
*/

View File

@@ -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)
}
}
*/

View File

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

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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()
}
}
*/

View File

@@ -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()
}
}
*/

View File

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