@@ -59,7 +59,9 @@ struct ContentView: View {
|
|||||||
@State var friend_events: [NostrEvent] = []
|
@State var friend_events: [NostrEvent] = []
|
||||||
@State var notifications: [NostrEvent] = []
|
@State var notifications: [NostrEvent] = []
|
||||||
@State var active_profile: String? = nil
|
@State var active_profile: String? = nil
|
||||||
|
@State var active_event_id: String? = nil
|
||||||
@State var profile_open: Bool = false
|
@State var profile_open: Bool = false
|
||||||
|
@State var thread_open: Bool = false
|
||||||
|
|
||||||
// connect retry timer
|
// connect retry timer
|
||||||
let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
|
let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
|
||||||
@@ -151,6 +153,9 @@ struct ContentView: View {
|
|||||||
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
NavigationLink(destination: MaybeThreadView, isActive: $thread_open) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
switch selected_timeline {
|
switch selected_timeline {
|
||||||
case .home:
|
case .home:
|
||||||
PostingTimelineView
|
PostingTimelineView
|
||||||
@@ -178,6 +183,18 @@ struct ContentView: View {
|
|||||||
.navigationViewStyle(.stack)
|
.navigationViewStyle(.stack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var MaybeThreadView: some View {
|
||||||
|
Group {
|
||||||
|
if let evid = self.active_event_id {
|
||||||
|
let thread_model = ThreadModel(evid: evid, pool: damus!.pool)
|
||||||
|
ThreadView(thread: thread_model, damus: damus!)
|
||||||
|
.environmentObject(profiles)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var MaybeProfileView: some View {
|
var MaybeProfileView: some View {
|
||||||
Group {
|
Group {
|
||||||
if let pk = self.active_profile {
|
if let pk = self.active_profile {
|
||||||
@@ -226,7 +243,8 @@ struct ContentView: View {
|
|||||||
active_profile = ref.ref_id
|
active_profile = ref.ref_id
|
||||||
profile_open = true
|
profile_open = true
|
||||||
} else if ref.key == "e" {
|
} else if ref.key == "e" {
|
||||||
// TODO open event view
|
active_event_id = ref.ref_id
|
||||||
|
thread_open = true
|
||||||
}
|
}
|
||||||
case .filter:
|
case .filter:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -129,15 +129,71 @@ func parse_mention(_ p: Parser, tags: [[String]]) -> Mention? {
|
|||||||
return Mention(index: digit, type: kind, ref: ref)
|
return Mention(index: digit, type: kind, ref: ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
|
func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? {
|
||||||
let new_ev = NostrEvent(content: post.content, pubkey: pubkey)
|
var i: Int = 0
|
||||||
for id in post.references {
|
for tag in tags {
|
||||||
var tag = [id.key, id.ref_id]
|
if tag.count >= 2 {
|
||||||
if let relay_id = id.relay_id {
|
if tag[0] == type && tag[1] == id {
|
||||||
tag.append(relay_id)
|
return i
|
||||||
|
}
|
||||||
}
|
}
|
||||||
new_ev.tags.append(tag)
|
i += i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PostTags {
|
||||||
|
let blocks: [Block]
|
||||||
|
let tags: [[String]]
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse_mention_type(_ c: String) -> MentionType? {
|
||||||
|
if c == "e" {
|
||||||
|
return .event
|
||||||
|
} else if c == "p" {
|
||||||
|
return .pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert
|
||||||
|
func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
|
||||||
|
var new_tags = tags
|
||||||
|
var blocks: [Block] = []
|
||||||
|
|
||||||
|
for post_block in post_blocks {
|
||||||
|
switch post_block {
|
||||||
|
case .ref(let ref):
|
||||||
|
guard let mention_type = parse_mention_type(ref.key) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if let ind = find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) {
|
||||||
|
let mention = Mention(index: ind, type: mention_type, ref: ref)
|
||||||
|
let block = Block.mention(mention)
|
||||||
|
blocks.append(block)
|
||||||
|
} else {
|
||||||
|
let ind = new_tags.count
|
||||||
|
new_tags.append(refid_to_tag(ref))
|
||||||
|
let mention = Mention(index: ind, type: mention_type, ref: ref)
|
||||||
|
let block = Block.mention(mention)
|
||||||
|
blocks.append(block)
|
||||||
|
}
|
||||||
|
case .text(let txt):
|
||||||
|
blocks.append(Block.text(txt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PostTags(blocks: blocks, tags: new_tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
|
||||||
|
let tags = post.references.map(refid_to_tag)
|
||||||
|
let post_blocks = parse_post_blocks(content: post.content)
|
||||||
|
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
|
||||||
|
let content = render_blocks(blocks: post_tags.blocks)
|
||||||
|
let new_ev = NostrEvent(content: content, pubkey: pubkey, kind: 1, tags: post_tags.tags)
|
||||||
new_ev.calculate_id()
|
new_ev.calculate_id()
|
||||||
new_ev.sign(privkey: privkey)
|
new_ev.sign(privkey: privkey)
|
||||||
return new_ev
|
return new_ev
|
||||||
|
|||||||
@@ -7,24 +7,57 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
enum InitialEvent {
|
||||||
|
case event(NostrEvent)
|
||||||
|
case event_id(String)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .event(let ev):
|
||||||
|
return ev.id
|
||||||
|
case .event_id(let evid):
|
||||||
|
return evid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// manages the lifetime of a thread
|
/// manages the lifetime of a thread
|
||||||
class ThreadModel: ObservableObject {
|
class ThreadModel: ObservableObject {
|
||||||
@Published var event: NostrEvent
|
@Published var initial_event: InitialEvent
|
||||||
@Published var events: [NostrEvent] = []
|
@Published var events: [NostrEvent] = []
|
||||||
@Published var event_map: [String: Int] = [:]
|
@Published var event_map: [String: Int] = [:]
|
||||||
var replies: ReplyMap = ReplyMap()
|
var replies: ReplyMap = ReplyMap()
|
||||||
|
|
||||||
|
var event: NostrEvent? {
|
||||||
|
switch initial_event {
|
||||||
|
case .event(let ev):
|
||||||
|
return ev
|
||||||
|
case .event_id(let evid):
|
||||||
|
for event in events {
|
||||||
|
if event.id == evid {
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let pool: RelayPool
|
let pool: RelayPool
|
||||||
var sub_id = UUID().description
|
var sub_id = UUID().description
|
||||||
|
|
||||||
init(ev: NostrEvent, pool: RelayPool) {
|
init(evid: String, pool: RelayPool) {
|
||||||
self.event = ev
|
|
||||||
self.pool = pool
|
self.pool = pool
|
||||||
|
self.initial_event = .event_id(evid)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(event: NostrEvent, pool: RelayPool) {
|
||||||
|
self.pool = pool
|
||||||
|
self.initial_event = .event(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
self.pool.unsubscribe(sub_id: sub_id)
|
self.pool.unsubscribe(sub_id: sub_id)
|
||||||
print("unsubscribing from thread \(event.id) with sub_id \(sub_id)")
|
print("unsubscribing from thread \(initial_event.id) with sub_id \(sub_id)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func reset_events() {
|
func reset_events() {
|
||||||
@@ -50,11 +83,10 @@ class ThreadModel: ObservableObject {
|
|||||||
func set_active_event(_ ev: NostrEvent) {
|
func set_active_event(_ ev: NostrEvent) {
|
||||||
if should_resubscribe(ev) {
|
if should_resubscribe(ev) {
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
self.event = ev
|
self.initial_event = .event(ev)
|
||||||
add_event(ev)
|
|
||||||
subscribe()
|
subscribe()
|
||||||
} else {
|
} else {
|
||||||
self.event = ev
|
self.initial_event = .event(ev)
|
||||||
if events.count == 0 {
|
if events.count == 0 {
|
||||||
add_event(ev)
|
add_event(ev)
|
||||||
}
|
}
|
||||||
@@ -68,13 +100,20 @@ class ThreadModel: ObservableObject {
|
|||||||
//var likes_filter = NostrFilter.filter_kinds(7])
|
//var likes_filter = NostrFilter.filter_kinds(7])
|
||||||
|
|
||||||
// TODO: add referenced relays
|
// TODO: add referenced relays
|
||||||
ref_events.referenced_ids = event.referenced_ids.map { $0.ref_id }
|
switch self.initial_event {
|
||||||
ref_events.referenced_ids!.append(event.id)
|
case .event(let ev):
|
||||||
|
ref_events.referenced_ids = ev.referenced_ids.map { $0.ref_id }
|
||||||
|
ref_events.referenced_ids?.append(ev.id)
|
||||||
|
events_filter.ids = ref_events.referenced_ids!
|
||||||
|
events_filter.ids?.append(ev.id)
|
||||||
|
case .event_id(let evid):
|
||||||
|
events_filter.ids = [evid]
|
||||||
|
ref_events.referenced_ids = [evid]
|
||||||
|
}
|
||||||
|
|
||||||
//likes_filter.ids = ref_events.referenced_ids!
|
//likes_filter.ids = ref_events.referenced_ids!
|
||||||
events_filter.ids = ref_events.referenced_ids!
|
|
||||||
|
|
||||||
print("subscribing to thread \(event.id) with sub_id \(sub_id)")
|
print("subscribing to thread \(initial_event.id) with sub_id \(sub_id)")
|
||||||
pool.register_handler(sub_id: sub_id, handler: handle_event)
|
pool.register_handler(sub_id: sub_id, handler: handle_event)
|
||||||
pool.send(.subscribe(.init(filters: [ref_events, events_filter], sub_id: sub_id)))
|
pool.send(.subscribe(.init(filters: [ref_events, events_filter], sub_id: sub_id)))
|
||||||
}
|
}
|
||||||
@@ -97,6 +136,8 @@ class ThreadModel: ObservableObject {
|
|||||||
|
|
||||||
self.events.append(ev)
|
self.events.append(ev)
|
||||||
self.events = self.events.sorted { $0.created_at < $1.created_at }
|
self.events = self.events.sorted { $0.created_at < $1.created_at }
|
||||||
|
objectWillChange.send()
|
||||||
|
|
||||||
var i: Int = 0
|
var i: Int = 0
|
||||||
for ev in events {
|
for ev in events {
|
||||||
self.event_map[ev.id] = i
|
self.event_map[ev.id] = i
|
||||||
|
|||||||
@@ -359,6 +359,14 @@ func random_bytes(count: Int) -> Data {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refid_to_tag(_ ref: ReferencedId) -> [String] {
|
||||||
|
var tag = [ref.key, ref.ref_id]
|
||||||
|
if let relay_id = ref.relay_id {
|
||||||
|
tag.append(relay_id)
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
func tag_to_refid(_ tag: [String]) -> ReferencedId? {
|
func tag_to_refid(_ tag: [String]) -> ReferencedId? {
|
||||||
if tag.count == 0 {
|
if tag.count == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var is_active: Bool {
|
var is_active: Bool {
|
||||||
return thread.event.id == event.id
|
return thread.initial_event.id == event.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func prev_reply_is_same() -> String? {
|
func prev_reply_is_same() -> String? {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ struct ChatroomView: View {
|
|||||||
damus: damus
|
damus: damus
|
||||||
)
|
)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if thread.event.id == ev.id {
|
if thread.initial_event.id == ev.id {
|
||||||
//dismiss()
|
//dismiss()
|
||||||
toggle_thread_view()
|
toggle_thread_view()
|
||||||
} else {
|
} else {
|
||||||
@@ -37,13 +37,13 @@ struct ChatroomView: View {
|
|||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .select_quote)) { notif in
|
.onReceive(NotificationCenter.default.publisher(for: .select_quote)) { notif in
|
||||||
let ev = notif.object as! NostrEvent
|
let ev = notif.object as! NostrEvent
|
||||||
if ev.id != thread.event.id {
|
if ev.id != thread.initial_event.id {
|
||||||
thread.set_active_event(ev)
|
thread.set_active_event(ev)
|
||||||
}
|
}
|
||||||
scroll_to_event(scroller: scroller, id: ev.id, delay: 0, animate: true, anchor: .top)
|
scroll_to_event(scroller: scroller, id: ev.id, delay: 0, animate: true, anchor: .top)
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
scroll_to_event(scroller: scroller, id: thread.event.id, delay: 0.3, animate: true, anchor: .bottom)
|
scroll_to_event(scroller: scroller, id: thread.initial_event.id, delay: 0.3, animate: true, anchor: .bottom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ struct EventDetailView: View {
|
|||||||
case .event(let ev, let highlight):
|
case .event(let ev, let highlight):
|
||||||
EventView(event: ev, highlight: highlight, has_action_bar: true, damus: damus)
|
EventView(event: ev, highlight: highlight, has_action_bar: true, damus: damus)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if thread.event.id == ev.id {
|
if thread.initial_event.id == ev.id {
|
||||||
toggle_thread_view()
|
toggle_thread_view()
|
||||||
} else {
|
} else {
|
||||||
thread.set_active_event(ev)
|
thread.set_active_event(ev)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ struct EventView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
UIPasteboard.general.string = event.id
|
UIPasteboard.general.string = "&" + event.id
|
||||||
} label: {
|
} label: {
|
||||||
Label("Copy ID", systemImage: "tag")
|
Label("Copy ID", systemImage: "tag")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ struct TimelineView: View {
|
|||||||
.environmentObject(profiles)
|
.environmentObject(profiles)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let tv = ThreadView(thread: ThreadModel(ev: ev, pool: damus.pool), damus: damus)
|
let tv = ThreadView(thread: ThreadModel(event: ev, pool: damus.pool), damus: damus)
|
||||||
.environmentObject(profiles)
|
.environmentObject(profiles)
|
||||||
|
|
||||||
NavigationLink(destination: tv) {
|
NavigationLink(destination: tv) {
|
||||||
|
|||||||
@@ -48,6 +48,31 @@ class damusTests: XCTestCase {
|
|||||||
XCTAssertEqual(parsed.count, 0)
|
XCTAssertEqual(parsed.count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testPostWithMentions() throws {
|
||||||
|
let evid = "0000000000000000000000000000000000000000000000000000000000000005"
|
||||||
|
let pk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
|
||||||
|
let content = "this is a @\(pk) mention"
|
||||||
|
let reply_ref = ReferencedId(ref_id: evid, relay_id: nil, key: "e")
|
||||||
|
let post = NostrPost(content: content, references: [reply_ref])
|
||||||
|
let ev = post_to_event(post: post, privkey: evid, pubkey: pk)
|
||||||
|
|
||||||
|
XCTAssertEqual(ev.tags.count, 2)
|
||||||
|
XCTAssertEqual(ev.content, "this is a #[1] mention")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPostTags() throws {
|
||||||
|
let pk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
|
||||||
|
let content = "this is a @\(pk) mention"
|
||||||
|
let parsed = parse_post_blocks(content: content)
|
||||||
|
let post_tags = make_post_tags(post_blocks: parsed, tags: [])
|
||||||
|
|
||||||
|
XCTAssertEqual(post_tags.blocks.count, 3)
|
||||||
|
XCTAssertEqual(post_tags.tags.count, 1)
|
||||||
|
XCTAssertEqual(post_tags.tags[0].count, 2)
|
||||||
|
XCTAssertEqual(post_tags.tags[0][0], "p")
|
||||||
|
XCTAssertEqual(post_tags.tags[0][1], pk)
|
||||||
|
}
|
||||||
|
|
||||||
func testInvalidPostReference() throws {
|
func testInvalidPostReference() throws {
|
||||||
let pk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e24"
|
let pk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e24"
|
||||||
let content = "this is a @\(pk) mention"
|
let content = "this is a @\(pk) mention"
|
||||||
|
|||||||
Reference in New Issue
Block a user