mirror of https://github.com/lukechilds/damus.git
William Casarin
3 years ago
12 changed files with 519 additions and 107 deletions
@ -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 |
||||
|
} |
||||
|
} |
@ -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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
@ -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() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
*/ |
@ -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) |
||||
|
} |
||||
|
} |
||||
|
*/ |
@ -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() |
||||
|
} |
||||
|
} |
||||
|
*/ |
@ -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() |
||||
|
} |
||||
|
} |
||||
|
*/ |
Loading…
Reference in new issue