@@ -7,6 +7,12 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
|
||||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; };
|
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; };
|
||||||
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; };
|
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; };
|
||||||
@@ -57,6 +63,12 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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>"; };
|
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>"; };
|
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>"; };
|
4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
@@ -119,6 +131,15 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
4C0A3F8D280F63FF000448DE /* Models */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
|
||||||
|
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
|
||||||
|
);
|
||||||
|
path = Models;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
4C75EFA227FA576C0006080F /* Views */ = {
|
4C75EFA227FA576C0006080F /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -132,6 +153,10 @@
|
|||||||
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */,
|
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */,
|
||||||
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
|
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
|
||||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
|
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
|
||||||
|
4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */,
|
||||||
|
4C0A3F90280F6528000448DE /* ChatView.swift */,
|
||||||
|
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */,
|
||||||
|
4C0A3F96280F8E02000448DE /* ThreadView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -177,6 +202,7 @@
|
|||||||
4CE6DEE527F7A08100C66700 /* damus */ = {
|
4CE6DEE527F7A08100C66700 /* damus */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4C0A3F8D280F63FF000448DE /* Models */,
|
||||||
4C75EFAB28049CC80006080F /* Nostr */,
|
4C75EFAB28049CC80006080F /* Nostr */,
|
||||||
4C75EFA72804823E0006080F /* Info.plist */,
|
4C75EFA72804823E0006080F /* Info.plist */,
|
||||||
4C75EFA227FA576C0006080F /* Views */,
|
4C75EFA227FA576C0006080F /* Views */,
|
||||||
@@ -365,12 +391,16 @@
|
|||||||
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
|
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
|
||||||
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
||||||
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
||||||
|
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
|
||||||
|
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
|
||||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
||||||
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
||||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
||||||
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
|
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
|
||||||
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
|
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
|
||||||
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
||||||
|
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
|
||||||
|
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
|
||||||
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
|
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
|
||||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
||||||
@@ -378,7 +408,9 @@
|
|||||||
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
|
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
|
||||||
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
|
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
|
||||||
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */,
|
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */,
|
||||||
|
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
|
||||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
||||||
|
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
|
||||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
||||||
4C75EFB528049D790006080F /* Relay.swift in Sources */,
|
4C75EFB528049D790006080F /* Relay.swift in Sources */,
|
||||||
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
|
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
|
||||||
|
|||||||
19
damus/Models/ReplyMap.swift
Normal file
19
damus/Models/ReplyMap.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
93
damus/Models/ThreadModel.swift
Normal file
93
damus/Models/ThreadModel.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -79,6 +79,30 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
|
|||||||
return true
|
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 {
|
public func directly_references(_ id: String) -> Bool {
|
||||||
// conditions: if it only has 1 e ref
|
// conditions: if it only has 1 e ref
|
||||||
// OR it has more than 1 e ref, ignoring the first
|
// OR it has more than 1 e ref, ignoring the first
|
||||||
|
|||||||
151
damus/Views/ChatView.swift
Normal file
151
damus/Views/ChatView.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
45
damus/Views/ChatroomView.swift
Normal file
45
damus/Views/ChatroomView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -29,68 +29,16 @@ enum CollapsedEvent: Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct EventDetailView: View {
|
struct EventDetailView: View {
|
||||||
@State var event: NostrEvent
|
|
||||||
|
|
||||||
let sub_id = UUID().description
|
let sub_id = UUID().description
|
||||||
|
|
||||||
@State var events: [NostrEvent] = []
|
@StateObject var thread: ThreadModel
|
||||||
@State var has_event: [String: ()] = [:]
|
|
||||||
@State var collapsed: Bool = true
|
@State var collapsed: Bool = true
|
||||||
|
|
||||||
@EnvironmentObject var profiles: Profiles
|
@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) {
|
func toggle_collapse_thread(scroller: ScrollViewProxy, id mid: String?, animate: Bool = true, anchor: UnitPoint = .center) {
|
||||||
self.collapsed = !self.collapsed
|
self.collapsed = !self.collapsed
|
||||||
if let id = mid {
|
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 {
|
func OurEventView(proxy: ScrollViewProxy, ev: NostrEvent, highlight: Highlight, collapsed_events: [CollapsedEvent]) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if ev.id == event.id {
|
if ev.id == thread.event.id {
|
||||||
EventView(event: ev, highlight: .main, has_action_bar: true)
|
EventView(event: ev, highlight: .main, has_action_bar: true)
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
scroll_to_event(scroller: proxy, id: ev.id, delay: 0.5, animate: true)
|
scroll_to_event(scroller: proxy, id: ev.id, delay: 0.5, animate: true)
|
||||||
@@ -134,7 +70,7 @@ struct EventDetailView: View {
|
|||||||
if !collapsed {
|
if !collapsed {
|
||||||
toggle_collapse_thread(scroller: proxy, id: ev.id)
|
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)
|
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))...'")
|
print("uncollapsing section at \(c.start) '\(ev.content.prefix(12))...'")
|
||||||
let start_id = ev.id
|
let start_id = ev.id
|
||||||
|
|
||||||
@@ -153,40 +89,25 @@ struct EventDetailView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
let collapsed_events = calculated_collapsed_events(collapsed: self.collapsed, active: self.event, events: self.events)
|
let collapsed_events = calculated_collapsed_events(collapsed: self.collapsed, active: thread.event, events: thread.events)
|
||||||
ForEach(collapsed_events, id: \.id) { cev in
|
ForEach(collapsed_events, id: \.id) { cev in
|
||||||
switch cev {
|
switch cev {
|
||||||
case .collapsed(let c):
|
case .collapsed(let c):
|
||||||
Text("··· \(c.count) other replies ···")
|
Text("··· \(c.count) other replies ···")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
self.uncollapse_section(scroller: proxy, c: c)
|
self.uncollapse_section(scroller: proxy, c: c)
|
||||||
//self.toggle_collapse_thread(scroller: proxy, id: nil)
|
//self.toggle_collapse_thread(scroller: proxy, id: nil)
|
||||||
}
|
}
|
||||||
case .event(let ev, let highlight):
|
case .event(let ev, let highlight):
|
||||||
OurEventView(proxy: proxy, ev: ev, highlight: highlight, collapsed_events: collapsed_events)
|
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) {
|
func print_event(_ ev: NostrEvent) {
|
||||||
print(ev.description)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CachedAsyncImage
|
|
||||||
|
|
||||||
enum Highlight {
|
enum Highlight {
|
||||||
case none
|
case none
|
||||||
@@ -95,7 +94,7 @@ func format_relative_time(_ created_at: Int64) -> String
|
|||||||
func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
|
func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
|
||||||
let (pubkeys, n) = event.reply_description
|
let (pubkeys, n) = event.reply_description
|
||||||
if pubkeys.count == 0 {
|
if pubkeys.count == 0 {
|
||||||
return "Reply"
|
return "Reply to self"
|
||||||
}
|
}
|
||||||
|
|
||||||
let names: [String] = pubkeys.map {
|
let names: [String] = pubkeys.map {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CachedAsyncImage
|
|
||||||
|
|
||||||
let PFP_SIZE: CGFloat? = 64
|
let PFP_SIZE: CGFloat? = 64
|
||||||
let CORNER_RADIUS: CGFloat = 32
|
let CORNER_RADIUS: CGFloat = 32
|
||||||
@@ -42,13 +41,13 @@ struct ProfilePicView: View {
|
|||||||
} placeholder: {
|
} placeholder: {
|
||||||
Color.purple.opacity(0.2)
|
Color.purple.opacity(0.2)
|
||||||
}
|
}
|
||||||
.frame(width: PFP_SIZE, height: PFP_SIZE)
|
.frame(width: size, height: size)
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
||||||
.padding(2)
|
.padding(2)
|
||||||
} else {
|
} else {
|
||||||
Color.purple.opacity(0.2)
|
Color.purple.opacity(0.2)
|
||||||
.frame(width: PFP_SIZE, height: PFP_SIZE)
|
.frame(width: size, height: size)
|
||||||
.cornerRadius(CORNER_RADIUS)
|
.cornerRadius(CORNER_RADIUS)
|
||||||
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
||||||
.padding(2)
|
.padding(2)
|
||||||
|
|||||||
64
damus/Views/ReplyQuoteView.swift
Normal file
64
damus/Views/ReplyQuoteView.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
44
damus/Views/ThreadView.swift
Normal file
44
damus/Views/ThreadView.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -17,10 +17,18 @@ struct TimelineView: View {
|
|||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(events, id: \.id) { (ev: NostrEvent) in
|
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")
|
.navigationBarTitle("Thread")
|
||||||
.padding([.leading, .trailing], 6)
|
.padding([.leading, .trailing], 6)
|
||||||
.environmentObject(profiles)
|
.environmentObject(profiles)
|
||||||
|
*/
|
||||||
|
|
||||||
|
let evdet = ThreadView(thread: ThreadModel(event: ev, pool: pool))
|
||||||
|
.navigationBarTitle("Chat")
|
||||||
|
.padding([.leading, .trailing], 6)
|
||||||
|
.environmentObject(profiles)
|
||||||
|
|
||||||
NavigationLink(destination: evdet) {
|
NavigationLink(destination: evdet) {
|
||||||
EventView(event: ev, highlight: .none, has_action_bar: true)
|
EventView(event: ev, highlight: .none, has_action_bar: true)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user