damus

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

commit b4140dc5f240920a1f92f66e260a25591ab4740c
parent 795577a0a1065dee1ba74040f9ce109d659f1826
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 20 Feb 2023 09:11:39 -0800

Add a "load more" button instead of always inserting events in timelines

Changelog-Added: Add a "load more" button instead of always inserting events in timelines

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 20++++++++++++++++++++
Mdamus/ContentView.swift | 6+++---
Mdamus/Models/HomeModel.swift | 28+++++++++++++++-------------
Mdamus/Models/ProfileModel.swift | 6++++--
Mdamus/Models/SearchHomeModel.swift | 11++++++-----
Mdamus/Models/SearchModel.swift | 7++++---
Adamus/Util/EventHolder.swift | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Util/InsertSort.swift | 7+++++++
Mdamus/Views/ChatView.swift | 2+-
Mdamus/Views/ProfileView.swift | 4++--
Mdamus/Views/SearchHomeView.swift | 4++--
Mdamus/Views/SearchView.swift | 2+-
Adamus/Views/Timeline/InnerTimelineView.swift | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/Timeline/LoadMoreButton.swift | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/TimelineView.swift | 115++++++++++++++++++++++++++++++++++++-------------------------------------------
15 files changed, 322 insertions(+), 94 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -156,6 +156,9 @@ 4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; }; 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.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 */; }; + 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; }; 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; }; 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; }; 4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; }; @@ -468,6 +471,9 @@ 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>"; }; 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>"; }; + 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; }; 4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; }; 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; }; 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; }; @@ -684,6 +690,7 @@ 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( + 4CE0E2B029A3DF4700DB4CA2 /* Timeline */, 4CE879562996C44A00F758CC /* Zaps */, 4CB9D4A52992D01900A9A7E4 /* Profile */, 4CAAD8AE29888A9B00060CEA /* Relays */, @@ -796,6 +803,7 @@ 3AB72AB8298ECF30004BB58C /* Translator.swift */, 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */, 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */, + 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */, ); path = Util; sourceTree = "<group>"; @@ -858,6 +866,15 @@ path = Events; sourceTree = "<group>"; }; + 4CE0E2B029A3DF4700DB4CA2 /* Timeline */ = { + isa = PBXGroup; + children = ( + 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */, + 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */, + ); + path = Timeline; + sourceTree = "<group>"; + }; 4CE4F9DF285287A000C00DD9 /* Components */ = { isa = PBXGroup; children = ( @@ -1231,6 +1248,8 @@ F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */, 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */, 4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */, + 4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */, + 4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */, 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */, 4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */, 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */, @@ -1298,6 +1317,7 @@ 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */, 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, + 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -92,7 +92,7 @@ struct ContentView: View { @State var filter_state : FilterState = .posts_and_replies @State private var isSideBarOpened = false @StateObject var home: HomeModel = HomeModel() - + // connect retry timer let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect() @@ -136,7 +136,7 @@ struct ContentView: View { func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View { ZStack { if let damus = self.damus_state { - TimelineView(events: $home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter) + TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter) } } } @@ -192,7 +192,7 @@ struct ContentView: View { case .notifications: VStack(spacing: 0) { Divider() - TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true }) + TimelineView(events: home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true }) } case .dms: DirectMessagesView(damus_state: damus_state!) diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -50,19 +50,22 @@ class HomeModel: ObservableObject { let profiles_subid = UUID().description @Published var new_events: NewEventsBits = NewEventsBits() - @Published var notifications: [NostrEvent] = [] + @Published var notifications: EventHolder @Published var dms: DirectMessagesModel - @Published var events: [NostrEvent] = [] + @Published var events: EventHolder @Published var loading: Bool = false @Published var signal: SignalModel = SignalModel() init() { + self.events = EventHolder() + self.notifications = EventHolder() self.damus_state = DamusState.empty - self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey) - self.setup_debouncer() + self.dms = DirectMessagesModel(our_pubkey: "") } - + init(damus_state: DamusState) { + self.events = EventHolder() + self.notifications = EventHolder() self.damus_state = damus_state self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey) self.setup_debouncer() @@ -140,7 +143,7 @@ class HomeModel: ObservableObject { return } - if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) { + if !notifications.insert(ev) { return } @@ -192,9 +195,9 @@ class HomeModel: ObservableObject { } func filter_muted() { - self.events = events.filter { !damus_state.contacts.is_muted($0.pubkey) } + events.filter { !damus_state.contacts.is_muted($0.pubkey) } self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) } - self.notifications = notifications.filter { !damus_state.contacts.is_muted($0.pubkey) } + notifications.filter { !damus_state.contacts.is_muted($0.pubkey) } } func handle_delete_event(_ ev: NostrEvent) { @@ -319,7 +322,7 @@ class HomeModel: ObservableObject { dms.append(contentsOf: incoming_dms) load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state) } else if sub_id == notifications_subid { - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications, damus_state: damus_state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications.all_events, damus_state: damus_state) } self.loading = false @@ -458,10 +461,10 @@ class HomeModel: ObservableObject { return } - if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) { + if !notifications.insert(ev) { return } - + handle_last_event(ev: ev, timeline: .notifications) } @@ -472,8 +475,7 @@ class HomeModel: ObservableObject { } func insert_home_event(_ ev: NostrEvent) { - let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at }) - if ok { + if events.insert(ev) { handle_last_event(ev: ev, timeline: .home) } } diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift @@ -8,7 +8,7 @@ import Foundation class ProfileModel: ObservableObject, Equatable { - @Published var events: [NostrEvent] = [] + var events: EventHolder = EventHolder() @Published var contacts: NostrEvent? = nil @Published var following: Int = 0 @Published var relays: [String: RelayInfo]? = nil @@ -111,7 +111,9 @@ class ProfileModel: ObservableObject, Equatable { return } if ev.is_textlike || ev.known_kind == .boost { - insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at}) + if self.events.insert(ev) { + self.objectWillChange.send() + } } else if ev.known_kind == .contacts { handle_profile_contact_event(ev) } else if ev.known_kind == .metadata { diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift @@ -10,7 +10,7 @@ import Foundation /// The data model for the SearchHome view, typically something global-like class SearchHomeModel: ObservableObject { - @Published var events: [NostrEvent] = [] + var events: EventHolder = EventHolder() @Published var loading: Bool = false var seen_pubkey: Set<String> = Set() @@ -31,7 +31,8 @@ class SearchHomeModel: ObservableObject { } func filter_muted() { - events = events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) } + events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) } + self.objectWillChange.send() } func subscribe() { @@ -61,8 +62,8 @@ class SearchHomeModel: ObservableObject { } seen_pubkey.insert(ev.pubkey) - insert_uniq_sorted_event(events: &events, new_ev: ev) { - $0.created_at > $1.created_at + if self.events.insert(ev) { + self.objectWillChange.send() } } case .notice(let msg): @@ -75,7 +76,7 @@ class SearchHomeModel: ObservableObject { // global events are not realtime unsubscribe(to: relay_id) - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events.all_events, damus_state: damus_state) } diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift @@ -9,7 +9,7 @@ import Foundation class SearchModel: ObservableObject { - @Published var events: [NostrEvent] = [] + var events: EventHolder = EventHolder() @Published var loading: Bool = false @Published var channel_name: String? = nil @@ -26,7 +26,8 @@ class SearchModel: ObservableObject { } func filter_muted() { - self.events = self.events.filter { should_show_event(contacts: contacts, ev: $0) } + self.events.filter { should_show_event(contacts: contacts, ev: $0) } + self.objectWillChange.send() } func subscribe() { @@ -57,7 +58,7 @@ class SearchModel: ObservableObject { return } - if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at } ) { + if self.events.insert(ev) { objectWillChange.send() } } diff --git a/damus/Util/EventHolder.swift b/damus/Util/EventHolder.swift @@ -0,0 +1,95 @@ +// +// EventHolder.swift +// damus +// +// Created by William Casarin on 2023-02-19. +// + +import Foundation + +/// Used for holding back events until they're ready to be displayed +class EventHolder: ObservableObject { + private var has_event: Set<String> + @Published var events: [NostrEvent] + @Published var incoming: [NostrEvent] + @Published var should_queue: Bool + + var queued: Int { + return incoming.count + } + + var has_incoming: Bool { + return queued > 0 + } + + var all_events: [NostrEvent] { + events + incoming + } + + init() { + self.should_queue = false + self.events = [] + self.incoming = [] + self.has_event = Set() + } + + init(events: [NostrEvent], incoming: [NostrEvent]) { + self.should_queue = false + self.events = events + self.incoming = incoming + self.has_event = Set() + } + + func filter(_ isIncluded: (NostrEvent) -> Bool) { + self.events = self.events.filter(isIncluded) + self.incoming = self.incoming.filter(isIncluded) + } + + func insert(_ ev: NostrEvent) -> Bool { + if should_queue { + return insert_queued(ev) + } else { + return insert_immediate(ev) + } + } + + private func insert_immediate(_ ev: NostrEvent) -> Bool { + if has_event.contains(ev.id) { + return false + } + + has_event.insert(ev.id) + + if insert_uniq_sorted_event_created(events: &self.events, new_ev: ev) { + return true + } + + return false + } + + private func insert_queued(_ ev: NostrEvent) -> Bool { + if has_event.contains(ev.id) { + return false + } + + has_event.insert(ev.id) + + incoming.append(ev) + return true + } + + func flush() { + var changed = false + for event in incoming { + if insert_uniq_sorted_event_created(events: &events, new_ev: event) { + changed = true + } + } + + if changed { + self.objectWillChange.send() + } + + self.incoming = [] + } +} diff --git a/damus/Util/InsertSort.swift b/damus/Util/InsertSort.swift @@ -59,6 +59,13 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool { return true } + +func insert_uniq_sorted_event_created(events: inout [NostrEvent], new_ev: NostrEvent) -> Bool { + return insert_uniq_sorted_event(events: &events, new_ev: new_ev) { + $0.created_at > $1.created_at + } +} + @discardableResult func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool { var i: Int = 0 diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift @@ -94,7 +94,7 @@ struct ChatView: View { } } - if let ref_id = thread.replies.lookup(event.id) { + if let _ = thread.replies.lookup(event.id) { if !is_reply_to_prev() { /* ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews) diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift @@ -404,10 +404,10 @@ struct ProfileView: View { .background(colorScheme == .dark ? Color.black : Color.white) if filter_state == FilterState.posts { - InnerTimelineView(events: $profile.events, damus: damus_state, show_friend_icon: false, filter: FilterState.posts.filter) + InnerTimelineView(events: profile.events, damus: damus_state, show_friend_icon: false, filter: FilterState.posts.filter) } if filter_state == FilterState.posts_and_replies { - InnerTimelineView(events: $profile.events, damus: damus_state, show_friend_icon: false, filter: FilterState.posts_and_replies.filter) + InnerTimelineView(events: profile.events, damus: damus_state, show_friend_icon: false, filter: FilterState.posts_and_replies.filter) } } .padding(.horizontal, Theme.safeAreaInsets?.left) diff --git a/damus/Views/SearchHomeView.swift b/damus/Views/SearchHomeView.swift @@ -40,7 +40,7 @@ struct SearchHomeView: View { } var GlobalContent: some View { - return TimelineView(events: $model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: { _ in true }) + return TimelineView(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: { _ in true }) .refreshable { // Fetch new information by unsubscribing and resubscribing to the relay model.unsubscribe() @@ -90,7 +90,7 @@ struct SearchHomeView: View { self.model.filter_muted() } .onAppear { - if model.events.isEmpty { + if model.events.events.isEmpty { model.subscribe() } } diff --git a/damus/Views/SearchView.swift b/damus/Views/SearchView.swift @@ -13,7 +13,7 @@ struct SearchView: View { @Environment(\.dismiss) var dismiss var body: some View { - TimelineView(events: $search.events, loading: $search.loading, damus: appstate, show_friend_icon: true, filter: { _ in true }) + TimelineView(events: search.events, loading: $search.loading, damus: appstate, show_friend_icon: true, filter: { _ in true }) .navigationBarTitle(describe_search(search.search)) .onReceive(handle_notify(.switched_timeline)) { obj in dismiss() diff --git a/damus/Views/Timeline/InnerTimelineView.swift b/damus/Views/Timeline/InnerTimelineView.swift @@ -0,0 +1,59 @@ +// +// InnerTimelineView.swift +// damus +// +// Created by William Casarin on 2023-02-20. +// + +import SwiftUI + + +struct InnerTimelineView: View { + @ObservedObject var events: EventHolder + let damus: DamusState + let show_friend_icon: Bool + let filter: (NostrEvent) -> Bool + @State var nav_target: NostrEvent? = nil + @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() + } + } + } + + var body: some View { + NavigationLink(destination: MaybeBuildThreadView, isActive: $navigating) { + EmptyView() + } + LazyVStack(spacing: 0) { + let events = self.events.events + if events.isEmpty { + EmptyTimelineView() + } else { + ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in + EventView(damus: damus, event: ev, has_action_bar: true) + .onTapGesture { + nav_target = ev + navigating = true + } + .padding(.top, 10) + } + } + } + .padding(.horizontal) + } +} + + +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) + .frame(width: 300, height: 500) + .border(Color.red) + } +} diff --git a/damus/Views/Timeline/LoadMoreButton.swift b/damus/Views/Timeline/LoadMoreButton.swift @@ -0,0 +1,50 @@ +// +// LoadMoreButton.swift +// damus +// +// Created by William Casarin on 2023-02-20. +// + +import SwiftUI + +struct LoadMoreButton: View { + @ObservedObject var events: EventHolder + let scroller: ScrollViewProxy? + + func click() { + events.flush() + guard let ev = events.events.first, let scroller else { + return + } + scroll_to_event(scroller: scroller, id: ev.id, delay: 0.1, animate: true) + } + + var body: some View { + Group { + if events.queued > 0 { + Button(action: click) { + Text("Load \(events.queued) more") + } + .font(.system(size: 14, weight: .bold)) + .padding(10) + .frame(height: 30) + .foregroundColor(.white) + .background(LINEAR_GRADIENT) + .clipShape(Capsule()) + } else { + EmptyView() + } + } + } +} + +struct LoadMoreButton_Previews: PreviewProvider { + @StateObject static var events: EventHolder = test_event_holder + + static var previews: some View { + LoadMoreButton(events: events, scroller: nil) + } +} + + +let test_event_holder = EventHolder(events: [], incoming: [test_event]) diff --git a/damus/Views/TimelineView.swift b/damus/Views/TimelineView.swift @@ -12,50 +12,12 @@ enum TimelineAction { case navigating } -struct InnerTimelineView: View { - @Binding var events: [NostrEvent] - let damus: DamusState - let show_friend_icon: Bool - let filter: (NostrEvent) -> Bool - @State var nav_target: NostrEvent? = nil - @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() - } - } - } - - var body: some View { - NavigationLink(destination: MaybeBuildThreadView, isActive: $navigating) { - EmptyView() - } - LazyVStack(spacing: 0) { - if events.isEmpty { - EmptyTimelineView() - } else { - ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in - EventView(damus: damus, event: ev, has_action_bar: true) - .onTapGesture { - nav_target = ev - navigating = true - } - .padding(.top, 10) - } - } - } - .padding(.horizontal) - } -} - struct TimelineView: View { - - @Binding var events: [NostrEvent] + @ObservedObject var events: EventHolder @Binding var loading: Bool + @State var offset = CGFloat.zero + + @Environment(\.colorScheme) var colorScheme let damus: DamusState let show_friend_icon: Bool @@ -65,37 +27,66 @@ struct TimelineView: View { MainContent } + func handle_scroll(_ proxy: GeometryProxy) { + let offset = -proxy.frame(in: .named("scroll")).origin.y + guard offset != -0.0 else { + return + } + self.events.should_queue = offset > 0 + } + + var realtime_bar_opacity: Double { + colorScheme == .dark ? 0.2 : 0.1 + } + var MainContent: some View { ScrollViewReader { scroller in - ScrollView { - InnerTimelineView(events: loading ? .constant(Constants.EXAMPLE_EVENTS) : $events, damus: damus, show_friend_icon: show_friend_icon, filter: loading ? { _ in true } : filter) - .redacted(reason: loading ? .placeholder : []) - .shimmer(loading) - .disabled(loading) - } - .onReceive(NotificationCenter.default.publisher(for: .scroll_to_top)) { _ in - guard let event = events.filter(self.filter).first else { - return + ZStack { + VStack { + LoadMoreButton(events: events, scroller: scroller) + .padding([.top], 10) + Spacer() + } + .zIndex(10.0) + + ScrollView { + InnerTimelineView(events: events, damus: damus, show_friend_icon: show_friend_icon, filter: loading ? { _ in true } : filter) + .redacted(reason: loading ? .placeholder : []) + .shimmer(loading) + .disabled(loading) + .background(GeometryReader { proxy -> Color in + DispatchQueue.main.async { + handle_scroll(proxy) + } + return Color.clear + }) + } + .overlay( + Rectangle() + .fill(RECTANGLE_GRADIENT.opacity(realtime_bar_opacity)) + .offset(y: -1) + .frame(height: events.should_queue ? 0 : 8) + , + alignment: .top + ) + .buttonStyle(BorderlessButtonStyle()) + .coordinateSpace(name: "scroll") + .onReceive(NotificationCenter.default.publisher(for: .scroll_to_top)) { _ in + guard let event = events.events.filter(self.filter).first else { + return + } + scroll_to_event(scroller: scroller, id: event.id, delay: 0.0, animate: true, anchor: .top) } - scroll_to_event(scroller: scroller, id: event.id, delay: 0.0, animate: true, anchor: .top) } } } } struct TimelineView_Previews: PreviewProvider { + @StateObject static var events = test_event_holder static var previews: some View { - TimelineView(events: .constant(Constants.EXAMPLE_EVENTS), loading: .constant(true), damus: Constants.EXAMPLE_DEMOS, show_friend_icon: true, filter: { _ in true }) + TimelineView(events: events, loading: .constant(true), damus: Constants.EXAMPLE_DEMOS, show_friend_icon: true, filter: { _ in true }) } } -struct NavigationLazyView<Content: View>: View { - let build: () -> Content - init(_ build: @autoclosure @escaping () -> Content) { - self.build = build - } - var body: Content { - build() - } -}