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