damus

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

commit d1cbf74840dbc30ef3f9fcf7b747de69ae8599b1
parent 538ce45c2c89dd3a058742fdf252bebfc1ce4937
Author: Thomas <31560900+0xtlt@users.noreply.github.com>
Date:   Sun, 25 Dec 2022 17:31:58 +0100

Improved visual composition for threads

Changelog-Changed: Improve visual composition of threads

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4++--
Mdamus/ContentView.swift | 3+--
Mdamus/Nostr/NostrEvent.swift | 14+++++++++++---
Mdamus/Views/ChatView.swift | 2+-
Mdamus/Views/DMView.swift | 2+-
Mdamus/Views/EventActionBar.swift | 3+--
Mdamus/Views/EventView.swift | 222++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mdamus/Views/NoteContentView.swift | 8+++++---
Mdamus/Views/ProfileName.swift | 12++++++++----
Mdamus/Views/ReplyQuoteView.swift | 2+-
Mdamus/Views/SearchResultsView.swift | 13+++++++++----
Adamus/Views/ThreadV2View.swift | 314+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/TimelineView.swift | 11+++++++----
14 files changed, 564 insertions(+), 50 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -131,6 +131,7 @@ 4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */; }; 6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; }; E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; }; + E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -309,6 +310,7 @@ 4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowView.swift; sourceTree = "<group>"; }; 4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventActionBar.swift; sourceTree = "<group>"; }; E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; }; + E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadV2View.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -478,6 +480,7 @@ 4C216F33286F5ACD00040376 /* DMView.swift */, 4C06670028FC7C5900038D2A /* RelayView.swift */, E990020E2955F837003BBC5A /* EditMetadataView.swift */, + E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */, ); path = Views; sourceTree = "<group>"; @@ -792,6 +795,7 @@ 4C363A9A28283854006E126D /* Reply.swift in Sources */, 4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */, 4C3EA66828FF5F9900C48A62 /* hex.c in Sources */, + E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */, 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */, 4C75EFB128049D510006080F /* NostrResponse.swift in Sources */, 4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */, diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -40,8 +40,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SparrowTek/Vault", "state" : { - "revision" : "f5707fac23f4a17b3e5ed32dd444f502773615ae", - "version" : "1.0.2" + "revision" : "87db56c3c8b6421c65b0745f73e08b0dc56f79d4", + "version" : "1.0.3" } } ], diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -166,8 +166,7 @@ struct ContentView: View { var MaybeThreadView: some View { Group { if let evid = self.active_event_id { - let thread_model = ThreadModel(evid: evid, damus_state: damus_state!) - ThreadView(thread: thread_model, damus: damus_state!, is_chatroom: false) + BuildThreadV2View(damus: damus_state!, event_id: evid) } else { EmptyView() } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -45,11 +45,19 @@ struct EventId: Identifiable, CustomStringConvertible { } } -class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable { +class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Hashable, Comparable { static func == (lhs: NostrEvent, rhs: NostrEvent) -> Bool { return lhs.id == rhs.id } + static func < (lhs: NostrEvent, rhs: NostrEvent) -> Bool { + return lhs.created_at < rhs.created_at + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + var id: String var sig: String var tags: [[String]] @@ -264,7 +272,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable { return (self.flags & 1) != 0 } - init(content: String, pubkey: String, kind: Int = 1, tags: [[String]] = []) { + init(content: String, pubkey: String, kind: Int = 1, tags: [[String]] = [], createdAt: Int64 = Int64(Date().timeIntervalSince1970)) { self.id = "" self.sig = "" @@ -272,7 +280,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable { self.pubkey = pubkey self.kind = kind self.tags = tags - self.created_at = Int64(Date().timeIntervalSince1970) + self.created_at = createdAt } /// Intiialization statement used to specificy ID diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift @@ -106,7 +106,7 @@ struct ChatView: View { } } - NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, show_images: should_show_images(contacts: damus_state.contacts, ev: event), artifacts: .just_content(event.content)) + NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, show_images: should_show_images(contacts: damus_state.contacts, ev: event), artifacts: .just_content(event.content), size: .normal) if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey { let bar = make_actionbar_model(ev: event, damus: damus_state) diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift @@ -21,7 +21,7 @@ struct DMView: View { Spacer() } - NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, show_images: should_show_images(contacts: damus_state.contacts, ev: event), artifacts: .just_content(event.get_content(damus_state.keypair.privkey))) + NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, show_images: should_show_images(contacts: damus_state.contacts, ev: event), artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), size: .normal) .foregroundColor(is_ours ? Color.white : Color.primary) .padding(10) .background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15)) diff --git a/damus/Views/EventActionBar.swift b/damus/Views/EventActionBar.swift @@ -86,7 +86,6 @@ struct EventActionBar: View { } */ } - .padding(.top, 1) .alert("Boost", isPresented: $confirm_boost) { Button("Cancel") { confirm_boost = false @@ -139,7 +138,7 @@ struct EventActionBar: View { func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View { Button(action: action) { - Label("", systemImage: img) + Label("&nbsp;", systemImage: img) .font(.footnote.weight(.medium)) .foregroundColor(col == nil ? Color.gray : col!) } diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -36,6 +36,90 @@ enum Highlight { } } +enum EventViewKind { + case small + case normal + case big + case selected +} + +func eventviewsize_to_font(_ size: EventViewKind) -> Font { + switch size { + case .small: + return .caption + case .normal: + return .body + case .big: + return .headline + case .selected: + return .headline + } +} + +struct BuilderEventView: View { + let damus: DamusState + let event_id: String + @State var event: NostrEvent? + @State var subscription_uuid: String = UUID().description + + func unsubscribe() { + damus.pool.unsubscribe(sub_id: subscription_uuid) + } + + func subscribe(filters: [NostrFilter]) { + damus.pool.register_handler(sub_id: subscription_uuid, handler: handle_event) + damus.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid))) + } + + func handle_event(relay_id: String, ev: NostrConnectionEvent) { + guard case .nostr_event(let nostr_response) = ev else { + return + } + + guard case .event(let id, let nostr_event) = nostr_response else { + return + } + + // Is current event + if id == subscription_uuid { + if event != nil { + return + } + + event = nostr_event + + unsubscribe() + } + } + + func load() { + subscribe(filters: [ + NostrFilter( + ids: [self.event_id], + limit: 1 + ) + ]) + } + + var body: some View { + VStack { + if event == nil { + ProgressView().padding() + } else { + NavigationLink(destination: BuildThreadV2View(damus: damus, event_id: event!.id)) { + EventView(damus: damus, event: event!, show_friend_icon: true, size: .small, embedded: true) + }.buttonStyle(.plain) + } + } + .frame(minWidth: 0, maxWidth: .infinity) + .border(Color.gray.opacity(0.2), width: 1) + .cornerRadius(2) + .onAppear { + self.load() + } + } +} + struct EventView: View { let event: NostrEvent let highlight: Highlight @@ -43,34 +127,42 @@ struct EventView: View { let damus: DamusState let pubkey: String let show_friend_icon: Bool + let size: EventViewKind + let embedded: Bool @EnvironmentObject var action_bar: ActionBarModel - init(event: NostrEvent, highlight: Highlight, has_action_bar: Bool, damus: DamusState, show_friend_icon: Bool) { + init(event: NostrEvent, highlight: Highlight, has_action_bar: Bool, damus: DamusState, show_friend_icon: Bool, size: EventViewKind = .normal, embedded: Bool = false) { self.event = event self.highlight = highlight self.has_action_bar = has_action_bar self.damus = damus self.pubkey = event.pubkey self.show_friend_icon = show_friend_icon + self.size = size + self.embedded = embedded } - init(damus: DamusState, event: NostrEvent, show_friend_icon: Bool) { + init(damus: DamusState, event: NostrEvent, show_friend_icon: Bool, size: EventViewKind = .normal, embedded: Bool = false) { self.event = event self.highlight = .none self.has_action_bar = false self.damus = damus self.pubkey = event.pubkey self.show_friend_icon = show_friend_icon + self.size = size + self.embedded = embedded } - init(damus: DamusState, event: NostrEvent, pubkey: String, show_friend_icon: Bool) { + init(damus: DamusState, event: NostrEvent, pubkey: String, show_friend_icon: Bool, size: EventViewKind = .normal, embedded: Bool = false) { self.event = event self.highlight = .none self.has_action_bar = false self.damus = damus self.pubkey = pubkey self.show_friend_icon = show_friend_icon + self.size = size + self.embedded = embedded } var body: some View { @@ -108,25 +200,44 @@ struct EventView: View { func TextEvent(_ event: NostrEvent, pubkey: String) -> some View { let content = event.get_content(damus.keypair.privkey) + return HStack(alignment: .top) { let profile = damus.profiles.lookup(id: pubkey) - VStack { - let pmodel = ProfileModel(pubkey: pubkey, damus: damus) - let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey)) - - NavigationLink(destination: pv) { - ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: highlight, profiles: damus.profiles) + + if size != .selected { + VStack { + let pmodel = ProfileModel(pubkey: pubkey, damus: damus) + let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey)) + + if !embedded { + NavigationLink(destination: pv) { + ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: highlight, profiles: damus.profiles) + } + } + + Spacer() } - - Spacer() } VStack(alignment: .leading) { HStack(alignment: .center) { - EventProfileName(pubkey: pubkey, profile: profile, contacts: damus.contacts, show_friend_confirmed: show_friend_icon) - Text("\(format_relative_time(event.created_at))") - .font(.body) - .foregroundColor(.gray) + if size == .selected { + VStack { + let pmodel = ProfileModel(pubkey: pubkey, damus: damus) + let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey)) + + NavigationLink(destination: pv) { + ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: highlight, profiles: damus.profiles) + } + } + } + + EventProfileName(pubkey: pubkey, profile: profile, contacts: damus.contacts, show_friend_confirmed: show_friend_icon, size: size) + if size != .selected { + Text("\(format_relative_time(event.created_at))") + .font(eventviewsize_to_font(size)) + .foregroundColor(.gray) + } } if event.is_reply(damus.keypair.privkey) { @@ -136,16 +247,55 @@ struct EventView: View { .frame(maxWidth: .infinity, alignment: .leading) } - NoteContentView(privkey: damus.keypair.privkey, event: event, profiles: damus.profiles, show_images: should_show_images(contacts: damus.contacts, ev: event), artifacts: .just_content(content)) + NoteContentView(privkey: damus.keypair.privkey, event: event, profiles: damus.profiles, show_images: should_show_images(contacts: damus.contacts, ev: event), artifacts: .just_content(content), size: self.size) .frame(maxWidth: .infinity, alignment: .leading) - - if has_action_bar { - let bar = make_actionbar_model(ev: event, damus: damus) - EventActionBar(damus_state: damus, event: event, bar: bar) + .allowsHitTesting(!embedded) + + if !embedded { + let blocks = event.blocks(damus.keypair.privkey).filter { block in + guard case .mention(let mention) = block else { + return false + } + + guard case .event = mention.type else { + return false + } + + if mention.ref.key != "e" { + return false + } + + + return true + } + + /// MARK: - Preview + if let firstBlock = blocks.first, case .mention(let mention) = firstBlock, mention.ref.key == "e" { + BuilderEventView(damus: damus, event_id: mention.ref.id) + } } - Divider() - .padding([.top], 4) + if !embedded { + if has_action_bar { + if size == .selected { + Text("\(format_date(event.created_at))") + .padding(.top, 10) + .font(.footnote) + .foregroundColor(.gray) + + Divider() + .padding([.bottom], 4) + } else { + Rectangle().frame(height: 2).opacity(0) + } + + let bar = make_actionbar_model(ev: event, damus: damus) + EventActionBar(damus_state: damus, event: event, bar: bar) + } + + Divider() + .padding([.top], 4) + } } .padding([.leading], 2) } @@ -231,6 +381,15 @@ func format_relative_time(_ created_at: Int64) -> String return time_ago_since(Date(timeIntervalSince1970: Double(created_at))) } +func format_date(_ created_at: Int64) -> String { + let date = Date(timeIntervalSince1970: TimeInterval(created_at)) + let dateFormatter = DateFormatter() + dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .short + return dateFormatter.string(from: date) +} + + func reply_desc(profiles: Profiles, event: NostrEvent) -> String { let desc = make_reply_description(event.tags) let pubkeys = desc.pubkeys @@ -285,6 +444,23 @@ func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel { struct EventView_Previews: PreviewProvider { static var previews: some View { - EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true) + VStack { + EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .small) + EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .normal) + EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .big) + + EventView( + event: NostrEvent( + content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", + pubkey: "pk", + createdAt: Int64(Date().timeIntervalSince1970 - 100) + ), + highlight: .none, + has_action_bar: true, + damus: test_damus_state(), + show_friend_icon: true, + size: .selected + ) + } } } diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -57,6 +57,8 @@ struct NoteContentView: View { @State var artifacts: NoteArtifacts + let size: EventViewKind + func MainContent() -> some View { let md_opts: AttributedString.MarkdownParsingOptions = .init(interpretedSyntax: .inlineOnlyPreservingWhitespace) @@ -64,10 +66,10 @@ struct NoteContentView: View { return VStack(alignment: .leading) { if let txt = try? AttributedString(markdown: artifacts.content, options: md_opts) { Text(txt) - .font(.body) + .font(eventviewsize_to_font(size)) } else { Text(artifacts.content) - .font(.body) + .font(eventviewsize_to_font(size)) } if show_images && artifacts.images.count > 0 { ImageCarousel(urls: artifacts.images) @@ -126,6 +128,6 @@ struct NoteContentView_Previews: PreviewProvider { let state = test_damus_state() let content = "hi there https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg" let artifacts = NoteArtifacts(content: content, images: [], invoices: []) - NoteContentView(privkey: "", event: NostrEvent(content: content, pubkey: "pk"), profiles: state.profiles, show_images: true, artifacts: artifacts) + NoteContentView(privkey: "", event: NostrEvent(content: content, pubkey: "pk"), profiles: state.profiles, show_images: true, artifacts: artifacts, size: .normal) } } diff --git a/damus/Views/ProfileName.swift b/damus/Views/ProfileName.swift @@ -103,20 +103,24 @@ struct EventProfileName: View { @State var display_name: String? - init(pubkey: String, profile: Profile?, contacts: Contacts, show_friend_confirmed: Bool) { + let size: EventViewKind + + init(pubkey: String, profile: Profile?, contacts: Contacts, show_friend_confirmed: Bool, size: EventViewKind = .normal) { self.pubkey = pubkey self.profile = profile self.prefix = "" self.contacts = contacts self.show_friend_confirmed = show_friend_confirmed + self.size = size } - init(pubkey: String, profile: Profile?, prefix: String, contacts: Contacts, show_friend_confirmed: Bool) { + init(pubkey: String, profile: Profile?, prefix: String, contacts: Contacts, show_friend_confirmed: Bool, size: EventViewKind = .normal) { self.pubkey = pubkey self.profile = profile self.prefix = prefix self.contacts = contacts self.show_friend_confirmed = show_friend_confirmed + self.size = size } var friend_icon: String? { @@ -143,10 +147,10 @@ struct EventProfileName: View { Text("@" + String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))) .foregroundColor(.gray) - .font(.body) + .font(eventviewsize_to_font(size)) } else { Text(String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))) - .font(.body) + .font(eventviewsize_to_font(size)) .fontWeight(.bold) } diff --git a/damus/Views/ReplyQuoteView.swift b/damus/Views/ReplyQuoteView.swift @@ -31,7 +31,7 @@ struct ReplyQuoteView: View { .foregroundColor(.gray) } - NoteContentView(privkey: privkey, event: event, profiles: profiles, show_images: false, artifacts: .just_content(event.content)) + NoteContentView(privkey: privkey, event: event, profiles: profiles, show_images: false, artifacts: .just_content(event.content), size: .normal) .font(.callout) .foregroundColor(.accentColor) diff --git a/damus/Views/SearchResultsView.swift b/damus/Views/SearchResultsView.swift @@ -53,8 +53,11 @@ struct SearchResultsView: View { let prof_model = ProfileModel(pubkey: h, damus: damus_state) let f = FollowersModel(damus_state: damus_state, target: h) let prof_view = ProfileView(damus_state: damus_state, profile: prof_model, followers: f) - let thread_model = ThreadModel(evid: h, damus_state: damus_state) - let ev_view = ThreadView(thread: thread_model, damus: damus_state, is_chatroom: false) + let ev_view = BuildThreadV2View( + damus: damus_state, + event_id: h + ) + VStack(spacing: 50) { NavigationLink(destination: prof_view) { Text("Goto profile \(h)") @@ -66,8 +69,10 @@ struct SearchResultsView: View { case .note(let nid): let decoded = try? bech32_decode(nid) let hex = hex_encode(decoded!.data) - let thread_model = ThreadModel(evid: hex, damus_state: damus_state) - let ev_view = ThreadView(thread: thread_model, damus: damus_state, is_chatroom: false) + let ev_view = BuildThreadV2View( + damus: damus_state, + event_id: hex + ) NavigationLink(destination: ev_view) { Text("Goto post \(nid)") } diff --git a/damus/Views/ThreadV2View.swift b/damus/Views/ThreadV2View.swift @@ -0,0 +1,314 @@ +// +// ThreadV2View.swift +// damus +// +// Created by Thomas Tastet on 25/12/2022. +// + +import SwiftUI + +struct ThreadV2 { + var parentEvents: [NostrEvent] + var current: NostrEvent + var childEvents: [NostrEvent] + + mutating func clean() { + // remove duplicates + self.parentEvents = Array(Set(self.parentEvents)) + self.childEvents = Array(Set(self.childEvents)) + + // remove empty contents + self.parentEvents = self.parentEvents.filter { event in + return !event.content.isEmpty + } + self.childEvents = self.childEvents.filter { event in + return !event.content.isEmpty + } + + // sort events by publication date + self.parentEvents = self.parentEvents.sorted { event1, event2 in + return event1 < event2 + } + self.childEvents = self.childEvents.sorted { event1, event2 in + return event1 < event2 + } + } +} + + +struct BuildThreadV2View: View { + let damus: DamusState + + @State var parents_ids: [String] = [] + let event_id: String + + @State var current_event: NostrEvent? = nil + + @State var thread: ThreadV2? = nil + + @State var current_events_uuid: String = "" + @State var childs_events_uuid: String = "" + @State var parents_events_uuids: [String] = [] + + @State var subscriptions_uuids: [String] = [] + + @Environment(\.dismiss) var dismiss + + init(damus: DamusState, event_id: String) { + self.damus = damus + self.event_id = event_id + } + + func unsubscribe_all() { + print("ThreadV2View: Unsubscribe all..") + + for subscriptions in subscriptions_uuids { + unsubscribe(subscriptions) + } + } + + func unsubscribe(_ sub_id: String) { + if subscriptions_uuids.contains(sub_id) { + damus.pool.unsubscribe(sub_id: sub_id) + + subscriptions_uuids.remove(at: subscriptions_uuids.firstIndex(of: sub_id)!) + } + } + + func subscribe(filters: [NostrFilter], sub_id: String = UUID().description) -> String { + damus.pool.register_handler(sub_id: sub_id, handler: handle_event) + damus.pool.send(.subscribe(.init(filters: filters, sub_id: sub_id))) + + subscriptions_uuids.append(sub_id) + + return sub_id + } + + func handle_event(relay_id: String, ev: NostrConnectionEvent) { + guard case .nostr_event(let nostr_response) = ev else { + return + } + + guard case .event(let id, let nostr_event) = nostr_response else { + return + } + + // Is current event + if id == current_events_uuid { + if current_event != nil { + return + } + + current_event = nostr_event + + thread = ThreadV2( + parentEvents: [], + current: current_event!, + childEvents: [] + ) + + // Get parents + parents_ids = current_event!.tags.enumerated().filter { (index, tag) in + return tag.count >= 2 && tag[0] == "e" && !current_event!.content.contains("#[\(index)]") + }.map { tag in + return tag.1[1] + } + + print("ThreadV2View: Parents list: (\(parents_ids)") + + if parents_ids.count > 0 { + // Ask for parents + let parents_events = NostrFilter( + ids: parents_ids, + limit: UInt32(parents_ids.count) + ) + + let uuid = subscribe(filters: [parents_events]) + parents_events_uuids.append(uuid) + print("ThreadV2View: Ask for parents (\(uuid)) (\(parents_events))") + } + + // Ask for children + let childs_events = NostrFilter( + referenced_ids: [self.event_id], + limit: 50 + ) + childs_events_uuid = subscribe(filters: [childs_events]) + print("ThreadV2View: Ask for children (\(childs_events) (\(childs_events_uuid))") + + return + } + + if parents_events_uuids.contains(id) { + // We are filtering this later + thread!.parentEvents.append(nostr_event) + + // Get parents of parents + let local_parents_ids = nostr_event.tags.enumerated().filter { (index, tag) in + return tag.count >= 2 && tag[0] == "e" && !nostr_event.content.contains("#[\(index)]") + }.map { tag in + return tag.1[1] + }.filter { tag_id in + return !parents_ids.contains(tag_id) + } + + print("ThreadV2View: Sub Parents list: (\(local_parents_ids))") + + // Expand new parents id + parents_ids.append(contentsOf: local_parents_ids) + + if local_parents_ids.count > 0 { + // Ask for parents + let parents_events = NostrFilter( + ids: local_parents_ids, + limit: UInt32(local_parents_ids.count) + ) + let uuid = subscribe(filters: [parents_events]) + parents_events_uuids.append(uuid) + print("ThreadV2View: Ask for sub_parents (\(local_parents_ids)) \(uuid)") + } + + thread!.clean() + unsubscribe(id) + return + } + + if id == childs_events_uuid { + // We are filtering this later + thread!.childEvents.append(nostr_event) + + thread!.clean() + return + } + } + + func reload() { + self.unsubscribe_all() + print("ThreadV2View: Reload!") + + // Get the current event + current_events_uuid = subscribe(filters: [ + NostrFilter( + ids: [self.event_id], + limit: 1 + ) + ]) + print("subscribing to threadV2 \(event_id) with sub_id \(current_events_uuid)") + } + + var body: some View { + VStack { + if thread == nil { + ProgressView() + } else { + ThreadV2View(damus: damus, thread: thread!) + } + } + .onAppear { + if self.thread == nil { + self.reload() + } + } + .onDisappear { + self.unsubscribe_all() + } + .onReceive(handle_notify(.switched_timeline)) { n in + dismiss() + } + } +} + +struct ThreadV2View: View { + let damus: DamusState + let thread: ThreadV2 + + var body: some View { + ScrollViewReader { reader in + ScrollView { + VStack { + // MARK: - Parents events view + VStack { + ForEach(thread.parentEvents, id: \.id) { event in + NavigationLink(destination: BuildThreadV2View( + damus: damus, + event_id: event.id + )){ + EventView( + event: event, + highlight: .none, + has_action_bar: true, + damus: damus, + show_friend_icon: true, // TODO: change it + size: .small + ) + } + .buttonStyle(.plain) + .onAppear { + // TODO: find another solution to prevent layout shifting and layout blocking on large responses + reader.scrollTo("main", anchor: .center) + } + } + }.background(GeometryReader { geometry in + // get the height and width of the EventView view + let eventHeight = geometry.frame(in: .global).height + // let eventWidth = geometry.frame(in: .global).width + + // vertical gray line in the background + Rectangle() + .fill(Color.gray.opacity(0.5)) + .frame(width: 2, height: eventHeight) + .offset(x: 25, y: 40) + }) + + // MARK: - Actual event view + EventView( + event: thread.current, + highlight: .none, + has_action_bar: true, + damus: damus, + show_friend_icon: true, // TODO: change it + size: .selected + ).id("main") + + // MARK: - Responses of the actual event view + ForEach(thread.childEvents, id: \.id) { event in + NavigationLink(destination: BuildThreadV2View( + damus: damus, + event_id: event.id + )){ + EventView( + event: event, + highlight: .none, + has_action_bar: true, + damus: damus, + show_friend_icon: true, // TODO: change it + size: .small + ) + }.buttonStyle(.plain) + } + } + }.padding().navigationBarTitle("Thread") + } + } +} + +struct ThreadV2View_Previews: PreviewProvider { + static var previews: some View { + BuildThreadV2View(damus: test_damus_state(), event_id: "ac9fd97b53b0c1d22b3aea2a3d62e11ae393960f5f91ee1791987d60151339a7") + ThreadV2View( + damus: test_damus_state(), + thread: ThreadV2( + parentEvents: [ + NostrEvent(id: "1", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), + NostrEvent(id: "2", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), + NostrEvent(id: "3", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), + ], + current: NostrEvent(id: "4", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), + childEvents: [ + NostrEvent(id: "5", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), + NostrEvent(id: "6", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"), + ] + ) + ) + } +} diff --git a/damus/Views/TimelineView.swift b/damus/Views/TimelineView.swift @@ -24,11 +24,14 @@ struct InnerTimelineView: View { EmptyTimelineView() } else { ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in - let tm = ThreadModel(event: inner_event_or_self(ev: ev), damus_state: damus) - let is_chatroom = should_show_chatroom(ev) - let tv = ThreadView(thread: tm, damus: damus, is_chatroom: is_chatroom) + //let tm = ThreadModel(event: inner_event_or_self(ev: ev), damus_state: damus) + //let is_chatroom = should_show_chatroom(ev) + //let tv = ThreadView(thread: tm, damus: damus, is_chatroom: is_chatroom) - NavigationLink(destination: tv) { + NavigationLink(destination: BuildThreadV2View( + damus: damus, + event_id: ev.id + )) { EventView(event: ev, highlight: .none, has_action_bar: true, damus: damus, show_friend_icon: show_friend_icon) } .isDetailLink(true)