damus

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

commit d0a6c2e2e4ba3f9581e72acf7fec3ada8e97bec8
parent b58baca227596f72a734fdce1346350e5dffea76
Author: William Casarin <jb55@jb55.com>
Date:   Sat,  4 Mar 2023 14:33:01 -0500

Thread Caching

Changelog-Added: Threads now load instantly and are cached

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 24++++++++++++++++++++----
Mdamus/ContentView.swift | 63++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mdamus/Models/ReplyMap.swift | 21+++++++++++++++++----
Adamus/Models/Search/SearchResultsModel.swift | 12++++++++++++
Adamus/Models/SearchedEventView.swift | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Models/ThreadModel.swift | 178+++++++++++++++++++++++++------------------------------------------------------
Mdamus/Nostr/NostrEvent.swift | 10++++++++++
Mdamus/Nostr/NostrFilter.swift | 6+++++-
Mdamus/Util/EventCache.swift | 61++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mdamus/Views/ChatView.swift | 6+++---
Mdamus/Views/ChatroomView.swift | 11+++++++----
Mdamus/Views/EventDetailView.swift | 2+-
Mdamus/Views/Events/BuilderEventView.swift | 4+++-
Mdamus/Views/Events/MutedEventView.swift | 18+++---------------
Mdamus/Views/Notifications/EventGroupView.swift | 4+++-
Mdamus/Views/Notifications/NotificationItemView.swift | 2+-
Mdamus/Views/SearchResultsView.swift | 33+++++++++++++++------------------
Ddamus/Views/ThreadV2View.swift | 336-------------------------------------------------------------------------------
Adamus/Views/ThreadView.swift | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/Timeline/InnerTimelineView.swift | 23++++++++++++-----------
20 files changed, 431 insertions(+), 537 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -170,6 +170,8 @@ 4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; }; 4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; }; 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; }; + 4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7A829B29DD50078AA28 /* SearchResultsModel.swift */; }; + 4CCEB7AB29B2A1320078AA28 /* SearchedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AA29B2A1320078AA28 /* SearchedEventView.swift */; }; 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; }; 4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; }; 4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; }; @@ -235,7 +237,7 @@ BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; }; E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; }; - E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */; }; + E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; }; F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12C29A1855400E10810 /* BookmarksManager.swift */; }; F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12E29A18EF500E10810 /* BookmarksView.swift */; }; F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; }; @@ -510,6 +512,8 @@ 4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = "<group>"; }; 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; }; 4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; }; + 4CCEB7A829B29DD50078AA28 /* SearchResultsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsModel.swift; sourceTree = "<group>"; }; + 4CCEB7AA29B2A1320078AA28 /* SearchedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SearchedEventView.swift; path = ../../Models/SearchedEventView.swift; sourceTree = "<group>"; }; 4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; }; 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; }; 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; }; @@ -577,7 +581,7 @@ BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; }; DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.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>"; }; + E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; }; F75BA12C29A1855400E10810 /* BookmarksManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksManager.swift; sourceTree = "<group>"; }; F75BA12E29A18EF500E10810 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; }; F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; }; @@ -688,6 +692,7 @@ 4C0A3F8D280F63FF000448DE /* Models */ = { isa = PBXGroup; children = ( + 4CCEB7A729B29DC90078AA28 /* Search */, 4C54AA0829A55416003E4487 /* Notifications */, 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */, 4C0A3F8E280F640A000448DE /* ThreadModel.swift */, @@ -805,7 +810,7 @@ 4C363AA128296A7E006E126D /* SearchView.swift */, BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */, 4C3AC7A02835A81400E1F516 /* SetupView.swift */, - E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */, + E9E4ED0A295867B900DD7078 /* ThreadView.swift */, 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */, 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */, 647D9A8C2968520300A295DE /* SideMenuView.swift */, @@ -925,6 +930,7 @@ isa = PBXGroup; children = ( 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */, + 4CCEB7AA29B2A1320078AA28 /* SearchedEventView.swift */, 4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */, 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */, 4CC7AAF5297F1A6A00430951 /* EventBody.swift */, @@ -938,6 +944,14 @@ path = Events; sourceTree = "<group>"; }; + 4CCEB7A729B29DC90078AA28 /* Search */ = { + isa = PBXGroup; + children = ( + 4CCEB7A829B29DD50078AA28 /* SearchResultsModel.swift */, + ); + path = Search; + sourceTree = "<group>"; + }; 4CE0E2B029A3DF4700DB4CA2 /* Timeline */ = { isa = PBXGroup; children = ( @@ -1306,6 +1320,7 @@ 4C75EFB728049D990006080F /* RelayPool.swift in Sources */, 4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */, 4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */, + 4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */, 4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */, 4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */, 4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */, @@ -1355,7 +1370,7 @@ 4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */, 4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */, 4C3EA66828FF5F9900C48A62 /* hex.c in Sources */, - E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */, + E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */, 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */, 4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */, 9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */, @@ -1393,6 +1408,7 @@ 4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */, 4C3EA67528FF7A5A00C48A62 /* take.c in Sources */, 4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */, + 4CCEB7AB29B2A1320078AA28 /* SearchedEventView.swift in Sources */, 4C06670128FC7C5900038D2A /* RelayView.swift in Sources */, 4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */, 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -81,7 +81,7 @@ struct ContentView: View { @State var event: NostrEvent? = nil @State var active_profile: String? = nil @State var active_search: NostrFilter? = nil - @State var active_event_id: String? = nil + @State var active_event: NostrEvent = test_event @State var profile_open: Bool = false @State var thread_open: Bool = false @State var search_open: Bool = false @@ -176,7 +176,8 @@ struct ContentView: View { NavigationLink(destination: MaybeProfileView, isActive: $profile_open) { EmptyView() } - NavigationLink(destination: MaybeThreadView, isActive: $thread_open) { + let thread = ThreadModel(event: active_event, damus_state: damus_state!) + NavigationLink(destination: ThreadView(state: damus_state!, thread: thread), isActive: $thread_open) { EmptyView() } NavigationLink(destination: MaybeSearchView, isActive: $search_open) { @@ -223,16 +224,6 @@ struct ContentView: View { } } - var MaybeThreadView: some View { - Group { - if let evid = self.active_event_id { - BuildThreadV2View(damus: damus_state!, event_id: evid) - } else { - EmptyView() - } - } - } - var MaybeProfileView: some View { Group { if let pk = self.active_profile { @@ -352,7 +343,11 @@ struct ContentView: View { active_profile = ref.ref_id profile_open = true } else if ref.key == "e" { - active_event_id = ref.ref_id + find_event(state: damus_state!, evid: ref.ref_id, find_from: nil) { ev in + if let ev { + active_event = ev + } + } thread_open = true } case .filter(let filt): @@ -766,3 +761,45 @@ func setup_notifications() { } } + +func find_event(state: DamusState, evid: String, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) { + if let ev = state.events.lookup(evid) { + callback(ev) + return + } + + let subid = UUID().description + + var has_event = false + var filter = NostrFilter.filter_ids([ evid ]) + filter.limit = 1 + var attempts = 0 + + state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in + guard case .nostr_event(let ev) = res else { + return + } + + guard ev.subid == subid else { + return + } + + switch ev { + case .event(_, let ev): + has_event = true + callback(ev) + state.pool.unsubscribe(sub_id: subid) + case .eose: + if !has_event { + attempts += 1 + if attempts == state.pool.descriptors.count / 2 { + callback(nil) + } + state.pool.unsubscribe(sub_id: subid, to: [relay_id]) + } + case .notice(_): + break + } + + } +} diff --git a/damus/Models/ReplyMap.swift b/damus/Models/ReplyMap.swift @@ -8,12 +8,25 @@ import Foundation class ReplyMap { - var replies: [String: String] = [:] + var replies: [String: Set<String>] = [:] - func lookup(_ id: String) -> String? { + func lookup(_ id: String) -> Set<String>? { return replies[id] } - func add(id: String, reply_id: String) { - replies[id] = reply_id + + private func ensure_set(id: String) { + if replies[id] == nil { + replies[id] = Set() + } + } + + @discardableResult + func add(id: String, reply_id: String) -> Bool { + ensure_set(id: id) + if (replies[id]!).contains(reply_id) { + return false + } + replies[id]!.insert(reply_id) + return true } } diff --git a/damus/Models/Search/SearchResultsModel.swift b/damus/Models/Search/SearchResultsModel.swift @@ -0,0 +1,12 @@ +// +// SearchResultsModel.swift +// damus +// +// Created by William Casarin on 2023-03-03. +// + +import Foundation + + +class SearchResultsModel: ObservableObject { +} diff --git a/damus/Models/SearchedEventView.swift b/damus/Models/SearchedEventView.swift @@ -0,0 +1,53 @@ +// +// SearchedEventView.swift +// damus +// +// Created by William Casarin on 2023-03-03. +// + +import SwiftUI + +enum EventSearchState { + case searching + case not_found + case found(NostrEvent) +} + +struct SearchedEventView: View { + let state: DamusState + let event_id: String + @State var search_state: EventSearchState = .searching + + var body: some View { + Group { + switch search_state { + case .not_found: + Text("Event could not be found") + case .searching: + Text("Searching...") + case .found(let ev): + let thread = ThreadModel(event: ev, damus_state: state) + let dest = ThreadView(state: state, thread: thread) + NavigationLink(destination: dest) { + EventView(damus: state, event: ev) + } + .buttonStyle(.plain) + } + } + .onAppear { + find_event(state: state, evid: event_id, find_from: nil) { ev in + if let ev { + self.search_state = .found(ev) + } else { + self.search_state = .not_found + } + } + } + } +} + +struct SearchedEventView_Previews: PreviewProvider { + static var previews: some View { + SearchedEventView(state: test_damus_state(), event_id: "event_id") + } +} diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -30,162 +30,100 @@ enum InitialEvent { /// manages the lifetime of a thread class ThreadModel: ObservableObject { - @Published var initial_event: InitialEvent - @Published var events: [NostrEvent] = [] - @Published var event_map: [String: Int] = [:] + @Published var event: NostrEvent + var event_map: Set<NostrEvent> + @Published var loading: Bool = false 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 - } + init(event: NostrEvent, damus_state: DamusState) { + self.damus_state = damus_state + self.event_map = Set() + self.event = event + add_event(event, privkey: nil) } let damus_state: DamusState let profiles_subid = UUID().description - var base_subid = UUID().description - - init(evid: String, damus_state: DamusState) { - self.damus_state = damus_state - self.initial_event = .event_id(evid) - } + let base_subid = UUID().description + let meta_subid = UUID().description - init(event: NostrEvent, damus_state: DamusState) { - self.damus_state = damus_state - self.initial_event = .event(event) + var subids: [String] { + return [profiles_subid, base_subid, meta_subid] } func unsubscribe() { + self.damus_state.pool.remove_handler(sub_id: base_subid) + self.damus_state.pool.remove_handler(sub_id: meta_subid) + self.damus_state.pool.remove_handler(sub_id: profiles_subid) self.damus_state.pool.unsubscribe(sub_id: base_subid) - print("unsubscribing from thread \(initial_event.id) with sub_id \(base_subid)") + self.damus_state.pool.unsubscribe(sub_id: meta_subid) + self.damus_state.pool.unsubscribe(sub_id: profiles_subid) + print("unsubscribing from thread \(event.id) with sub_id \(base_subid)") } - func reset_events() { - self.events.removeAll() - self.event_map.removeAll() - self.replies.replies.removeAll() - } - - func should_resubscribe(_ ev_b: NostrEvent) -> Bool { - if self.events.count == 0 { - return true - } + @discardableResult + func set_active_event(_ ev: NostrEvent, privkey: String?) -> Bool { + self.event = ev + add_event(ev, privkey: privkey) - if ev_b.is_root_event() { - return false - } - - // rough heuristic to save us from resubscribing all the time - //return ev_b.count_ids() != self.event.count_ids() - return true - } - - func set_active_event(_ ev: NostrEvent, privkey: String?) { - if should_resubscribe(ev) { - unsubscribe() - self.initial_event = .event(ev) - subscribe() - } else { - self.initial_event = .event(ev) - if events.count == 0 { - add_event(ev, privkey: privkey) - } - } + //self.objectWillChange.send() + return false } func subscribe() { + var meta_events = NostrFilter() + var event_filter = NostrFilter() var ref_events = NostrFilter() - var events_filter = NostrFilter() //var likes_filter = NostrFilter.filter_kinds(7]) - // TODO: add referenced relays - switch self.initial_event { - case .event(let ev): - ref_events.referenced_ids = ev.referenced_ids.map { $0.ref_id } - ref_events.referenced_ids?.append(ev.id) - ref_events.limit = 50 - events_filter.ids = ref_events.referenced_ids ?? [] - events_filter.limit = 100 - events_filter.ids?.append(ev.id) - case .event_id(let evid): - ref_events.referenced_ids = [evid] - ref_events.limit = 50 - events_filter.ids = [evid] - events_filter.limit = 100 - } - - //likes_filter.ids = ref_events.referenced_ids! - - print("subscribing to thread \(initial_event.id) with sub_id \(base_subid)") - damus_state.pool.register_handler(sub_id: base_subid, handler: handle_event) - loading = true - damus_state.pool.send(.subscribe(.init(filters: [ref_events, events_filter], sub_id: base_subid))) - } - - func lookup(_ event_id: String) -> NostrEvent? { - if let i = event_map[event_id] { - return events[i] - } - return nil - } - - func add_event(_ ev: NostrEvent, privkey: String?) { - guard ev.should_show_event else { - return - } + let thread_id = event.thread_id(privkey: nil) - if event_map[ev.id] != nil { - return - } + ref_events.referenced_ids = [thread_id, event.id] + ref_events.kinds = [1] + ref_events.limit = 1000 - for reply in ev.direct_replies(privkey) { - self.replies.add(id: ev.id, reply_id: reply.ref_id) - } + event_filter.ids = [thread_id, event.id] - if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at < $1.created_at }) { - objectWillChange.send() - } - //self.events.append(ev) - //self.events = self.events.sorted { $0.created_at < $1.created_at } + meta_events.referenced_ids = [event.id] + meta_events.kinds = [9735, 1, 6, 7] + meta_events.limit = 1000 - var i: Int = 0 - for ev in events { - self.event_map[ev.id] = i - i += 1 - } - - if let evid = self.initial_event.is_event_id { - if ev.id == evid { - // this should trigger a resubscribe... - set_active_event(ev, privkey: privkey) + /* + if let last_ev = self.events.last { + if last_ev.created_at <= Int64(Date().timeIntervalSince1970) { + ref_events.since = last_ev.created_at } } + */ + let base_filters = [event_filter, ref_events] + let meta_filters = [meta_events] + + print("subscribing to thread \(event.id) with sub_id \(base_subid)") + loading = true + damus_state.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event) + damus_state.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event) } - func handle_channel_meta(_ ev: NostrEvent) { - guard let meta: ChatroomMetadata = decode_json(ev.content) else { + func add_event(_ ev: NostrEvent, privkey: String?) { + if event_map.contains(ev) { return } - notify(.chatroom_meta, meta) + let the_ev = damus_state.events.upsert(ev) + damus_state.events.add_replies(ev: the_ev) + + event_map.insert(ev) + objectWillChange.send() } func handle_event(relay_id: String, ev: NostrConnectionEvent) { let (sub_id, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: ev) { sid, ev in - guard sid == base_subid || sid == profiles_subid else { + guard subids.contains(sid) else { return } @@ -193,21 +131,19 @@ class ThreadModel: ObservableObject { process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) } else if ev.is_textlike { self.add_event(ev, privkey: self.damus_state.keypair.privkey) - } else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create { - handle_channel_meta(ev) } } - guard done && (sub_id == base_subid || sub_id == profiles_subid) else { + guard done, let sub_id, subids.contains(sub_id) else { return } - if (events.contains { ev in ev.id == initial_event.id }) { + if event_map.contains(event) { loading = false } if sub_id == self.base_subid { - load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: damus_state) + load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map)), damus_state: damus_state) } } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -215,6 +215,16 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has } } } + + public func thread_id(privkey: String?) -> String { + for ref in event_refs(privkey) { + if let thread_id = ref.is_thread_id { + return thread_id.ref_id + } + } + + return self.id + } public func last_refid() -> ReferencedId? { var mlast: Int? = nil diff --git a/damus/Nostr/NostrFilter.swift b/damus/Nostr/NostrFilter.swift @@ -43,7 +43,11 @@ struct NostrFilter: Codable, Equatable { public static var filter_text: NostrFilter { return filter_kinds([1]) } - + + public static func filter_ids(_ ids: [String]) -> NostrFilter { + return NostrFilter(ids: ids, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: nil) + } + public static var filter_profiles: NostrFilter { return filter_kinds([0]) } diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift @@ -9,6 +9,64 @@ import Foundation class EventCache { private var events: [String: NostrEvent] + private var replies: ReplyMap = ReplyMap() + + //private var thread_latest: [String: Int64] + + init() { + self.events = [:] + self.replies = ReplyMap() + } + + func parent_events(event: NostrEvent) -> [NostrEvent] { + var parents: [NostrEvent] = [] + + var ev = event + + while true { + guard let direct_reply = ev.direct_replies(nil).first else { + break + } + + guard let next_ev = lookup(direct_reply.ref_id), next_ev != ev else { + break + } + + parents.append(next_ev) + ev = next_ev + } + + return parents.reversed() + } + + func add_replies(ev: NostrEvent) { + for reply in ev.direct_replies(nil) { + replies.add(id: reply.ref_id, reply_id: ev.id) + } + } + + func child_events(event: NostrEvent) -> [NostrEvent] { + guard let xs = replies.lookup(event.id) else { + return [] + } + let evs: [NostrEvent] = xs.reduce(into: [], { evs, evid in + guard let ev = self.lookup(evid) else { + return + } + + evs.append(ev) + }).sorted(by: { $0.created_at < $1.created_at }) + return evs + } + + func upsert(_ ev: NostrEvent) -> NostrEvent { + if let found = lookup(ev.id) { + return found + } + + insert(ev) + return ev + } func lookup(_ evid: String) -> NostrEvent? { return events[evid] @@ -21,7 +79,4 @@ class EventCache { events[ev.id] = ev } - init() { - self.events = [:] - } } diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift @@ -35,14 +35,14 @@ struct ChatView: View { } if let rep = thread.replies.lookup(event.id) { - return rep == prev.id + return rep.contains(prev.id) } return false } var is_active: Bool { - return thread.initial_event.id == event.id + return thread.event.id == event.id } func prev_reply_is_same() -> String? { @@ -161,7 +161,7 @@ func prev_reply_is_same(event: NostrEvent, prev_ev: NostrEvent?, replies: ReplyM if let prev_reply_id = replies.lookup(prev.id) { if let cur_reply_id = replies.lookup(event.id) { if prev_reply_id != cur_reply_id { - return cur_reply_id + return cur_reply_id.first } } } diff --git a/damus/Views/ChatroomView.swift b/damus/Views/ChatroomView.swift @@ -7,6 +7,7 @@ import SwiftUI +/* struct ChatroomView: View { @EnvironmentObject var thread: ThreadModel @Environment(\.dismiss) var dismiss @@ -26,7 +27,7 @@ struct ChatroomView: View { ) .event_context_menu(ev, keypair: damus.keypair, target_pubkey: ev.pubkey, bookmarks: damus.bookmarks) .onTapGesture { - if thread.initial_event.id == ev.id { + if thread.event.id == ev.id { //dismiss() toggle_thread_view() } else { @@ -44,7 +45,7 @@ struct ChatroomView: View { } .onReceive(NotificationCenter.default.publisher(for: .select_quote)) { notif in let ev = notif.object as! NostrEvent - if ev.id != thread.initial_event.id { + if ev.id != thread.event.id { thread.set_active_event(ev, privkey: damus.keypair.privkey) } scroll_to_event(scroller: scroller, id: ev.id, delay: 0, animate: true) @@ -57,7 +58,7 @@ struct ChatroomView: View { once = true } .onAppear() { - scroll_to_event(scroller: scroller, id: thread.initial_event.id, delay: 0.1, animate: false) + scroll_to_event(scroller: scroller, id: thread.event.id, delay: 0.1, animate: false) } } } @@ -76,7 +77,9 @@ struct ChatroomView_Previews: PreviewProvider { static var previews: some View { let state = test_damus_state() ChatroomView(damus: state) - .environmentObject(ThreadModel(evid: "&849ab9bb263ed2819db06e05f1a1a3b72878464e8c7146718a2fc1bf1912f893", damus_state: state)) + .environmentObject(ThreadModel(event: test_event, damus_state: state)) } } + +*/ diff --git a/damus/Views/EventDetailView.swift b/damus/Views/EventDetailView.swift @@ -16,7 +16,7 @@ struct EventDetailView: View { func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) { if !thread.loading { - let id = thread.initial_event.id + let id = thread.event.id scroll_to_event(scroller: proxy, id: id, delay: 0.1, animate: false) } } diff --git a/damus/Views/Events/BuilderEventView.swift b/damus/Views/Events/BuilderEventView.swift @@ -62,7 +62,9 @@ struct BuilderEventView: View { VStack { if let event = event { let ev = event.inner_event ?? event - NavigationLink(destination: BuildThreadV2View(damus: damus, event_id: ev.id)) { + let thread = ThreadModel(event: ev, damus_state: damus) + let dest = ThreadView(state: damus, thread: thread) + NavigationLink(destination: dest) { EmbeddedEventView(damus_state: damus, event: event) .padding(8) }.buttonStyle(.plain) diff --git a/damus/Views/Events/MutedEventView.swift b/damus/Views/Events/MutedEventView.swift @@ -13,18 +13,14 @@ struct MutedEventView: View { let scroller: ScrollViewProxy? let selected: Bool - @Binding var nav_target: String? - @Binding var navigating: Bool @State var shown: Bool @Environment(\.colorScheme) var colorScheme - init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, nav_target: Binding<String?>, navigating: Binding<Bool>, selected: Bool) { + init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, selected: Bool) { self.damus_state = damus_state self.event = event self.scroller = scroller self.selected = selected - self._nav_target = nav_target - self._navigating = navigating self._shown = State(initialValue: should_show_event(contacts: damus_state.contacts, ev: event)) } @@ -58,14 +54,6 @@ struct MutedEventView: View { SelectedEventView(damus: damus_state, event: event) } else { EventView(damus: damus_state, event: event) - .onTapGesture { - nav_target = event.id - navigating = true - } - .onAppear { - // TODO: find another solution to prevent layout shifting and layout blocking on large responses - scroller?.scrollTo("main", anchor: .bottom) - } } } } @@ -101,12 +89,12 @@ struct MutedEventView: View { } struct MutedEventView_Previews: PreviewProvider { - @State static var nav_target: String? = nil + @State static var nav_target: NostrEvent = test_event @State static var navigating: Bool = false static var previews: some View { - MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, nav_target: $nav_target, navigating: $navigating, selected: false) + MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, selected: false) .frame(width: .infinity, height: 50) } } diff --git a/damus/Views/Notifications/EventGroupView.swift b/damus/Views/Notifications/EventGroupView.swift @@ -207,7 +207,9 @@ struct EventGroupView: View { GroupDescription if let event { - NavigationLink(destination: BuildThreadV2View(damus: state, event_id: event.id)) { + let thread = ThreadModel(event: event, damus_state: state) + let dest = ThreadView(state: state, thread: thread) + NavigationLink(destination: dest) { Text(event.content) .padding([.top], 1) .foregroundColor(.gray) diff --git a/damus/Views/Notifications/NotificationItemView.swift b/damus/Views/Notifications/NotificationItemView.swift @@ -51,7 +51,7 @@ struct NotificationItemView: View { EventGroupView(state: state, event: ev, group: .reaction(evgrp)) case .reply(let ev): - NavigationLink(destination: BuildThreadV2View(damus: state, event_id: ev.id)) { + NavigationLink(destination: ThreadView(state: state, thread: ThreadModel(event: ev, damus_state: state))) { EventView(damus: state, event: ev) } .buttonStyle(.plain) diff --git a/damus/Views/SearchResultsView.swift b/damus/Views/SearchResultsView.swift @@ -43,39 +43,36 @@ struct SearchResultsView: View { case .profile(let prof): let decoded = try? bech32_decode(prof) let hex = hex_encode(decoded!.data) - let prof_model = ProfileModel(pubkey: hex, damus: damus_state) - let f = FollowersModel(damus_state: damus_state, target: prof) - let dst = ProfileView(damus_state: damus_state, profile: prof_model, followers: f) - NavigationLink(destination: dst) { + let prof_view = ProfileView(damus_state: damus_state, pubkey: hex) + NavigationLink(destination: prof_view) { Text("Goto profile \(prof)", comment: "Navigation link to go to profile.") } case .hex(let h): - 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 ev_view = BuildThreadV2View( - damus: damus_state, - event_id: h - ) + let prof_view = ProfileView(damus_state: damus_state, pubkey: h) + //let ev_view = ThreadView(damus: damus_state, event_id: h) + NavigationLink(destination: prof_view) { + Text("Goto profile \(h)", comment: "Navigation link to go to profile referenced by hex code.") + } + /* VStack(spacing: 50) { - NavigationLink(destination: prof_view) { - Text("Goto profile \(h)", comment: "Navigation link to go to profile referenced by hex code.") - } NavigationLink(destination: ev_view) { Text("Goto post \(h)", comment: "Navigation link to go to post referenced by hex code.") } } + */ case .note(let nid): + /* let decoded = try? bech32_decode(nid) let hex = hex_encode(decoded!.data) - let ev_view = BuildThreadV2View( - damus: damus_state, - event_id: hex - ) + let ev_view = ThreadView(state: state, ev: ev) + */ + Text("Todo: fix this") + /* NavigationLink(destination: ev_view) { Text("Goto post \(nid)", comment: "Navigation link to go to post referenced by note ID.") } + */ case .none: Text("none", comment: "No search results.") } diff --git a/damus/Views/ThreadV2View.swift b/damus/Views/ThreadV2View.swift @@ -1,336 +0,0 @@ -// -// 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 extra_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_current_events(ev: NostrEvent) { - if current_event != nil { - return - } - - current_event = ev - - 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( - kinds: [1], - referenced_ids: [self.event_id], - limit: 50 - ) - childs_events_uuid = subscribe(filters: [childs_events]) - print("ThreadV2View: Ask for children (\(childs_events) (\(childs_events_uuid))") - } - - func handle_parent_events(sub_id: String, nostr_event: NostrEvent) { - - // 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(sub_id) - return - - } - - 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 { - handle_current_events(ev: nostr_event) - return - } - - if parents_events_uuids.contains(id) { - handle_parent_events(sub_id: id, nostr_event: nostr_event) - 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!") - - var extra = NostrFilter.filter_kinds([9735, 6, 7]) - extra.referenced_ids = [ self.event_id ] - - // Get the current event - current_events_uuid = subscribe(filters: [ - NostrFilter(ids: [self.event_id], limit: 1) - ]) - - extra_events_uuid = subscribe(filters: [extra]) - 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 - @State var nav_target: String? = nil - @State var navigating: Bool = false - - var MaybeBuildThreadView: some View { - Group { - if let evid = nav_target { - BuildThreadV2View(damus: damus, event_id: evid) - } else { - EmptyView() - } - } - } - - var body: some View { - NavigationLink(destination: MaybeBuildThreadView, isActive: $navigating) { - EmptyView() - } - ScrollViewReader { reader in - ScrollView { - VStack { - // MARK: - Parents events view - VStack { - ForEach(thread.parentEvents, id: \.id) { event in - MutedEventView(damus_state: damus, - event: event, - scroller: reader, - nav_target: $nav_target, - navigating: $navigating, - selected: false - ) - - Divider() - .padding(.top, 4) - .padding(.leading, 25 * 2) - } - }.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.25)) - .frame(width: 2, height: eventHeight) - .offset(x: 25, y: 40) - }) - - // MARK: - Actual event view - MutedEventView( - damus_state: damus, - event: thread.current, - scroller: reader, - nav_target: $nav_target, - navigating: $navigating, - selected: true - ).id("main") - - // MARK: - Responses of the actual event view - LazyVStack { - ForEach(thread.childEvents, id: \.id) { event in - MutedEventView( - damus_state: damus, - event: event, - scroller: nil, - nav_target: $nav_target, - navigating: $navigating, - selected: false - ) - - Divider() - .padding([.top], 4) - } - } - }.padding() - }.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note 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/ThreadView.swift b/damus/Views/ThreadView.swift @@ -0,0 +1,101 @@ +// +// ThreadV2View.swift +// damus +// +// Created by Thomas Tastet on 25/12/2022. +// + +import SwiftUI + +struct ThreadView: View { + let state: DamusState + + @StateObject var thread: ThreadModel + @Environment(\.dismiss) var dismiss + + var parent_events: [NostrEvent] { + state.events.parent_events(event: thread.event) + } + + var child_events: [NostrEvent] { + state.events.child_events(event: thread.event) + } + + var body: some View { + ScrollViewReader { reader in + ScrollView { + LazyVStack { + // MARK: - Parents events view + ForEach(parent_events, id: \.id) { parent_event in + MutedEventView(damus_state: state, + event: parent_event, + scroller: reader, + selected: false + ) + .onTapGesture { + thread.set_active_event(parent_event, privkey: state.keypair.privkey) + scroll_to_event(scroller: reader, id: parent_event.id, delay: 0.1, animate: false) + } + + Divider() + .padding(.top, 4) + .padding(.leading, 25 * 2) + }.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.25)) + .frame(width: 2, height: eventHeight) + .offset(x: 25, y: 40) + }) + + // MARK: - Actual event view + MutedEventView( + damus_state: state, + event: self.thread.event, + scroller: reader, + selected: true + ) + .id(self.thread.event.id) + + ForEach(child_events, id: \.id) { child_event in + MutedEventView( + damus_state: state, + event: child_event, + scroller: nil, + selected: false + ) + .onTapGesture { + thread.set_active_event(child_event, privkey: state.keypair.privkey) + scroll_to_event(scroller: reader, id: child_event.id, delay: 0.1, animate: false) + } + + Divider() + .padding([.top], 4) + } + }.padding() + }.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread.")) + .onAppear { + thread.subscribe() + scroll_to_event(scroller: reader, id: self.thread.event.id, delay: 0.0, animate: false) + } + .onDisappear { + thread.unsubscribe() + } + .onReceive(handle_notify(.switched_timeline)) { notif in + dismiss() + } + } + } +} + +struct ThreadView_Previews: PreviewProvider { + static var previews: some View { + let state = test_damus_state() + let thread = ThreadModel(event: test_event, damus_state: state) + ThreadView(state: state, thread: thread) + } +} diff --git a/damus/Views/Timeline/InnerTimelineView.swift b/damus/Views/Timeline/InnerTimelineView.swift @@ -13,21 +13,22 @@ struct InnerTimelineView: View { let damus: DamusState let show_friend_icon: Bool let filter: (NostrEvent) -> Bool - @State var nav_target: NostrEvent? = nil + @State var nav_target: NostrEvent @State var navigating: Bool = false - var MaybeBuildThreadView: some View { - Group { - if let ev = nav_target { - BuildThreadV2View(damus: damus, event_id: (ev.inner_event ?? ev).id) - } else { - EmptyView() - } - } + init(events: EventHolder, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool) { + self.events = events + self.damus = damus + self.show_friend_icon = show_friend_icon + self.filter = filter + // dummy event to avoid MaybeThreadView + self._nav_target = State(initialValue: test_event) } var body: some View { - NavigationLink(destination: MaybeBuildThreadView, isActive: $navigating) { + let thread = ThreadModel(event: nav_target, damus_state: damus) + let dest = ThreadView(state: damus, thread: thread) + NavigationLink(destination: dest, isActive: $navigating) { EmptyView() } LazyVStack(spacing: 0) { @@ -55,7 +56,7 @@ struct InnerTimelineView: View { struct InnerTimelineView_Previews: PreviewProvider { static var previews: some View { - InnerTimelineView(events: test_event_holder, damus: test_damus_state(), show_friend_icon: true, filter: { _ in true }, nav_target: nil, navigating: false) + InnerTimelineView(events: test_event_holder, damus: test_damus_state(), show_friend_icon: true, filter: { _ in true }) .frame(width: 300, height: 500) .border(Color.red) }