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:
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: ¬ifications, 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: ¬ifications, 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()
- }
-}