ThreadV2View.swift (11928B)
1 // 2 // ThreadV2View.swift 3 // damus 4 // 5 // Created by Thomas Tastet on 25/12/2022. 6 // 7 8 import SwiftUI 9 10 struct ThreadV2 { 11 var parentEvents: [NostrEvent] 12 var current: NostrEvent 13 var childEvents: [NostrEvent] 14 15 mutating func clean() { 16 // remove duplicates 17 self.parentEvents = Array(Set(self.parentEvents)) 18 self.childEvents = Array(Set(self.childEvents)) 19 20 // remove empty contents 21 self.parentEvents = self.parentEvents.filter { event in 22 return !event.content.isEmpty 23 } 24 self.childEvents = self.childEvents.filter { event in 25 return !event.content.isEmpty 26 } 27 28 // sort events by publication date 29 self.parentEvents = self.parentEvents.sorted { event1, event2 in 30 return event1 < event2 31 } 32 self.childEvents = self.childEvents.sorted { event1, event2 in 33 return event1 < event2 34 } 35 } 36 } 37 38 39 struct BuildThreadV2View: View { 40 let damus: DamusState 41 42 @State var parents_ids: [String] = [] 43 let event_id: String 44 45 @State var current_event: NostrEvent? = nil 46 47 @State var thread: ThreadV2? = nil 48 49 @State var current_events_uuid: String = "" 50 @State var extra_events_uuid: String = "" 51 @State var childs_events_uuid: String = "" 52 @State var parents_events_uuids: [String] = [] 53 54 @State var subscriptions_uuids: [String] = [] 55 56 @Environment(\.dismiss) var dismiss 57 58 init(damus: DamusState, event_id: String) { 59 self.damus = damus 60 self.event_id = event_id 61 } 62 63 func unsubscribe_all() { 64 print("ThreadV2View: Unsubscribe all..") 65 66 for subscriptions in subscriptions_uuids { 67 unsubscribe(subscriptions) 68 } 69 } 70 71 func unsubscribe(_ sub_id: String) { 72 if subscriptions_uuids.contains(sub_id) { 73 damus.pool.unsubscribe(sub_id: sub_id) 74 75 subscriptions_uuids.remove(at: subscriptions_uuids.firstIndex(of: sub_id)!) 76 } 77 } 78 79 func subscribe(filters: [NostrFilter], sub_id: String = UUID().description) -> String { 80 damus.pool.register_handler(sub_id: sub_id, handler: handle_event) 81 damus.pool.send(.subscribe(.init(filters: filters, sub_id: sub_id))) 82 83 subscriptions_uuids.append(sub_id) 84 85 return sub_id 86 } 87 88 func handle_current_events(ev: NostrEvent) { 89 if current_event != nil { 90 return 91 } 92 93 current_event = ev 94 95 thread = ThreadV2( 96 parentEvents: [], 97 current: current_event!, 98 childEvents: [] 99 ) 100 101 // Get parents 102 parents_ids = current_event!.tags.enumerated().filter { (index, tag) in 103 return tag.count >= 2 && tag[0] == "e" && !current_event!.content.contains("#[\(index)]") 104 }.map { tag in 105 return tag.1[1] 106 } 107 108 print("ThreadV2View: Parents list: (\(parents_ids)") 109 110 if parents_ids.count > 0 { 111 // Ask for parents 112 let parents_events = NostrFilter( 113 ids: parents_ids, 114 limit: UInt32(parents_ids.count) 115 ) 116 117 let uuid = subscribe(filters: [parents_events]) 118 parents_events_uuids.append(uuid) 119 print("ThreadV2View: Ask for parents (\(uuid)) (\(parents_events))") 120 } 121 122 // Ask for children 123 let childs_events = NostrFilter( 124 kinds: [1], 125 referenced_ids: [self.event_id], 126 limit: 50 127 ) 128 childs_events_uuid = subscribe(filters: [childs_events]) 129 print("ThreadV2View: Ask for children (\(childs_events) (\(childs_events_uuid))") 130 } 131 132 func handle_parent_events(sub_id: String, nostr_event: NostrEvent) { 133 134 // We are filtering this later 135 thread!.parentEvents.append(nostr_event) 136 137 // Get parents of parents 138 let local_parents_ids = nostr_event.tags.enumerated().filter { (index, tag) in 139 return tag.count >= 2 && tag[0] == "e" && !nostr_event.content.contains("#[\(index)]") 140 }.map { tag in 141 return tag.1[1] 142 }.filter { tag_id in 143 return !parents_ids.contains(tag_id) 144 } 145 146 print("ThreadV2View: Sub Parents list: (\(local_parents_ids))") 147 148 // Expand new parents id 149 parents_ids.append(contentsOf: local_parents_ids) 150 151 if local_parents_ids.count > 0 { 152 // Ask for parents 153 let parents_events = NostrFilter( 154 ids: local_parents_ids, 155 limit: UInt32(local_parents_ids.count) 156 ) 157 let uuid = subscribe(filters: [parents_events]) 158 parents_events_uuids.append(uuid) 159 print("ThreadV2View: Ask for sub_parents (\(local_parents_ids)) \(uuid)") 160 } 161 162 thread!.clean() 163 unsubscribe(sub_id) 164 return 165 166 } 167 168 func handle_event(relay_id: String, ev: NostrConnectionEvent) { 169 guard case .nostr_event(let nostr_response) = ev else { 170 return 171 } 172 173 guard case .event(let id, let nostr_event) = nostr_response else { 174 return 175 } 176 177 // Is current event 178 if id == current_events_uuid { 179 handle_current_events(ev: nostr_event) 180 return 181 } 182 183 if parents_events_uuids.contains(id) { 184 handle_parent_events(sub_id: id, nostr_event: nostr_event) 185 return 186 } 187 188 if id == childs_events_uuid { 189 // We are filtering this later 190 thread!.childEvents.append(nostr_event) 191 192 thread!.clean() 193 return 194 } 195 } 196 197 func reload() { 198 self.unsubscribe_all() 199 print("ThreadV2View: Reload!") 200 201 var extra = NostrFilter.filter_kinds([9735, 6, 7]) 202 extra.referenced_ids = [ self.event_id ] 203 204 // Get the current event 205 current_events_uuid = subscribe(filters: [ 206 NostrFilter(ids: [self.event_id], limit: 1) 207 ]) 208 209 extra_events_uuid = subscribe(filters: [extra]) 210 print("subscribing to threadV2 \(event_id) with sub_id \(current_events_uuid)") 211 } 212 213 var body: some View { 214 VStack { 215 if thread == nil { 216 ProgressView() 217 } else { 218 ThreadV2View(damus: damus, thread: thread!) 219 } 220 } 221 .onAppear { 222 if self.thread == nil { 223 self.reload() 224 } 225 } 226 .onDisappear { 227 self.unsubscribe_all() 228 } 229 .onReceive(handle_notify(.switched_timeline)) { n in 230 dismiss() 231 } 232 } 233 } 234 235 struct ThreadV2View: View { 236 let damus: DamusState 237 let thread: ThreadV2 238 @State var nav_target: String? = nil 239 @State var navigating: Bool = false 240 241 var MaybeBuildThreadView: some View { 242 Group { 243 if let evid = nav_target { 244 BuildThreadV2View(damus: damus, event_id: evid) 245 } else { 246 EmptyView() 247 } 248 } 249 } 250 251 var body: some View { 252 NavigationLink(destination: MaybeBuildThreadView, isActive: $navigating) { 253 EmptyView() 254 } 255 ScrollViewReader { reader in 256 ScrollView { 257 VStack { 258 // MARK: - Parents events view 259 VStack { 260 ForEach(thread.parentEvents, id: \.id) { event in 261 MutedEventView(damus_state: damus, 262 event: event, 263 scroller: reader, 264 nav_target: $nav_target, 265 navigating: $navigating, 266 selected: false 267 ) 268 269 Divider() 270 .padding(.top, 4) 271 .padding(.leading, 25 * 2) 272 } 273 }.background(GeometryReader { geometry in 274 // get the height and width of the EventView view 275 let eventHeight = geometry.frame(in: .global).height 276 // let eventWidth = geometry.frame(in: .global).width 277 278 // vertical gray line in the background 279 Rectangle() 280 .fill(Color.gray.opacity(0.25)) 281 .frame(width: 2, height: eventHeight) 282 .offset(x: 25, y: 40) 283 }) 284 285 // MARK: - Actual event view 286 MutedEventView( 287 damus_state: damus, 288 event: thread.current, 289 scroller: reader, 290 nav_target: $nav_target, 291 navigating: $navigating, 292 selected: true 293 ).id("main") 294 295 // MARK: - Responses of the actual event view 296 LazyVStack { 297 ForEach(thread.childEvents, id: \.id) { event in 298 MutedEventView( 299 damus_state: damus, 300 event: event, 301 scroller: nil, 302 nav_target: $nav_target, 303 navigating: $navigating, 304 selected: false 305 ) 306 307 Divider() 308 .padding([.top], 4) 309 } 310 } 311 }.padding() 312 }.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread.")) 313 } 314 } 315 } 316 317 struct ThreadV2View_Previews: PreviewProvider { 318 static var previews: some View { 319 BuildThreadV2View(damus: test_damus_state(), event_id: "ac9fd97b53b0c1d22b3aea2a3d62e11ae393960f5f91ee1791987d60151339a7") 320 ThreadV2View( 321 damus: test_damus_state(), 322 thread: ThreadV2( 323 parentEvents: [ 324 NostrEvent(id: "1", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), 325 NostrEvent(id: "2", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), 326 NostrEvent(id: "3", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), 327 ], 328 current: NostrEvent(id: "4", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), 329 childEvents: [ 330 NostrEvent(id: "5", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), 331 NostrEvent(id: "6", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), 332 ] 333 ) 334 ) 335 } 336 }