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:
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(" ", 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)