damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }