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
This commit is contained in:
William Casarin
2023-02-20 09:11:39 -08:00
parent 795577a0a1
commit b4140dc5f2
15 changed files with 322 additions and 94 deletions

View File

@@ -156,6 +156,9 @@
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; }; 4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; }; 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.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 */; }; 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; }; 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; };
@@ -684,6 +690,7 @@
4C75EFA227FA576C0006080F /* Views */ = { 4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
4CE879562996C44A00F758CC /* Zaps */, 4CE879562996C44A00F758CC /* Zaps */,
4CB9D4A52992D01900A9A7E4 /* Profile */, 4CB9D4A52992D01900A9A7E4 /* Profile */,
4CAAD8AE29888A9B00060CEA /* Relays */, 4CAAD8AE29888A9B00060CEA /* Relays */,
@@ -796,6 +803,7 @@
3AB72AB8298ECF30004BB58C /* Translator.swift */, 3AB72AB8298ECF30004BB58C /* Translator.swift */,
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */, 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */, 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */,
); );
path = Util; path = Util;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -858,6 +866,15 @@
path = Events; path = Events;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
4CE0E2B029A3DF4700DB4CA2 /* Timeline */ = {
isa = PBXGroup;
children = (
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */,
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */,
);
path = Timeline;
sourceTree = "<group>";
};
4CE4F9DF285287A000C00DD9 /* Components */ = { 4CE4F9DF285287A000C00DD9 /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1231,6 +1248,8 @@
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */, F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */, 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */, 4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */,
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */, 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */, 4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */, 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
@@ -1298,6 +1317,7 @@
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */, 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,

View File

@@ -92,7 +92,7 @@ struct ContentView: View {
@State var filter_state : FilterState = .posts_and_replies @State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false @State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel() @StateObject var home: HomeModel = HomeModel()
// connect retry timer // connect retry timer
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect() 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 { func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
ZStack { ZStack {
if let damus = self.damus_state { 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: case .notifications:
VStack(spacing: 0) { VStack(spacing: 0) {
Divider() 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: case .dms:
DirectMessagesView(damus_state: damus_state!) DirectMessagesView(damus_state: damus_state!)

View File

@@ -50,19 +50,22 @@ class HomeModel: ObservableObject {
let profiles_subid = UUID().description let profiles_subid = UUID().description
@Published var new_events: NewEventsBits = NewEventsBits() @Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications: [NostrEvent] = [] @Published var notifications: EventHolder
@Published var dms: DirectMessagesModel @Published var dms: DirectMessagesModel
@Published var events: [NostrEvent] = [] @Published var events: EventHolder
@Published var loading: Bool = false @Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel() @Published var signal: SignalModel = SignalModel()
init() { init() {
self.events = EventHolder()
self.notifications = EventHolder()
self.damus_state = DamusState.empty self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey) self.dms = DirectMessagesModel(our_pubkey: "")
self.setup_debouncer()
} }
init(damus_state: DamusState) { init(damus_state: DamusState) {
self.events = EventHolder()
self.notifications = EventHolder()
self.damus_state = damus_state self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey) self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.setup_debouncer() self.setup_debouncer()
@@ -140,7 +143,7 @@ class HomeModel: ObservableObject {
return return
} }
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) { if !notifications.insert(ev) {
return return
} }
@@ -192,9 +195,9 @@ class HomeModel: ObservableObject {
} }
func filter_muted() { 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.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) { func handle_delete_event(_ ev: NostrEvent) {
@@ -319,7 +322,7 @@ class HomeModel: ObservableObject {
dms.append(contentsOf: incoming_dms) dms.append(contentsOf: incoming_dms)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state) load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state)
} else if sub_id == notifications_subid { } 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 self.loading = false
@@ -458,10 +461,10 @@ class HomeModel: ObservableObject {
return return
} }
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) { if !notifications.insert(ev) {
return return
} }
handle_last_event(ev: ev, timeline: .notifications) handle_last_event(ev: ev, timeline: .notifications)
} }
@@ -472,8 +475,7 @@ class HomeModel: ObservableObject {
} }
func insert_home_event(_ ev: NostrEvent) { 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 events.insert(ev) {
if ok {
handle_last_event(ev: ev, timeline: .home) handle_last_event(ev: ev, timeline: .home)
} }
} }

View File

@@ -8,7 +8,7 @@
import Foundation import Foundation
class ProfileModel: ObservableObject, Equatable { class ProfileModel: ObservableObject, Equatable {
@Published var events: [NostrEvent] = [] var events: EventHolder = EventHolder()
@Published var contacts: NostrEvent? = nil @Published var contacts: NostrEvent? = nil
@Published var following: Int = 0 @Published var following: Int = 0
@Published var relays: [String: RelayInfo]? = nil @Published var relays: [String: RelayInfo]? = nil
@@ -111,7 +111,9 @@ class ProfileModel: ObservableObject, Equatable {
return return
} }
if ev.is_textlike || ev.known_kind == .boost { 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 { } else if ev.known_kind == .contacts {
handle_profile_contact_event(ev) handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata { } else if ev.known_kind == .metadata {

View File

@@ -10,7 +10,7 @@ import Foundation
/// The data model for the SearchHome view, typically something global-like /// The data model for the SearchHome view, typically something global-like
class SearchHomeModel: ObservableObject { class SearchHomeModel: ObservableObject {
@Published var events: [NostrEvent] = [] var events: EventHolder = EventHolder()
@Published var loading: Bool = false @Published var loading: Bool = false
var seen_pubkey: Set<String> = Set() var seen_pubkey: Set<String> = Set()
@@ -31,7 +31,8 @@ class SearchHomeModel: ObservableObject {
} }
func filter_muted() { 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() { func subscribe() {
@@ -61,8 +62,8 @@ class SearchHomeModel: ObservableObject {
} }
seen_pubkey.insert(ev.pubkey) seen_pubkey.insert(ev.pubkey)
insert_uniq_sorted_event(events: &events, new_ev: ev) { if self.events.insert(ev) {
$0.created_at > $1.created_at self.objectWillChange.send()
} }
} }
case .notice(let msg): case .notice(let msg):
@@ -75,7 +76,7 @@ class SearchHomeModel: ObservableObject {
// global events are not realtime // global events are not realtime
unsubscribe(to: relay_id) 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)
} }

View File

@@ -9,7 +9,7 @@ import Foundation
class SearchModel: ObservableObject { class SearchModel: ObservableObject {
@Published var events: [NostrEvent] = [] var events: EventHolder = EventHolder()
@Published var loading: Bool = false @Published var loading: Bool = false
@Published var channel_name: String? = nil @Published var channel_name: String? = nil
@@ -26,7 +26,8 @@ class SearchModel: ObservableObject {
} }
func filter_muted() { 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() { func subscribe() {
@@ -57,7 +58,7 @@ class SearchModel: ObservableObject {
return 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() objectWillChange.send()
} }
} }

View File

@@ -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 = []
}
}

View File

@@ -59,6 +59,13 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
return true 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 @discardableResult
func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool { func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool {
var i: Int = 0 var i: Int = 0

View File

@@ -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() { if !is_reply_to_prev() {
/* /*
ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews) ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews)

View File

@@ -404,10 +404,10 @@ struct ProfileView: View {
.background(colorScheme == .dark ? Color.black : Color.white) .background(colorScheme == .dark ? Color.black : Color.white)
if filter_state == FilterState.posts { 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 { 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) .padding(.horizontal, Theme.safeAreaInsets?.left)

View File

@@ -40,7 +40,7 @@ struct SearchHomeView: View {
} }
var GlobalContent: some 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 { .refreshable {
// Fetch new information by unsubscribing and resubscribing to the relay // Fetch new information by unsubscribing and resubscribing to the relay
model.unsubscribe() model.unsubscribe()
@@ -90,7 +90,7 @@ struct SearchHomeView: View {
self.model.filter_muted() self.model.filter_muted()
} }
.onAppear { .onAppear {
if model.events.isEmpty { if model.events.events.isEmpty {
model.subscribe() model.subscribe()
} }
} }

View File

@@ -13,7 +13,7 @@ struct SearchView: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var body: some View { 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)) .navigationBarTitle(describe_search(search.search))
.onReceive(handle_notify(.switched_timeline)) { obj in .onReceive(handle_notify(.switched_timeline)) { obj in
dismiss() dismiss()

View File

@@ -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)
}
}

View File

@@ -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])

View File

@@ -12,50 +12,12 @@ enum TimelineAction {
case navigating 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 { struct TimelineView: View {
@ObservedObject var events: EventHolder
@Binding var events: [NostrEvent]
@Binding var loading: Bool @Binding var loading: Bool
@State var offset = CGFloat.zero
@Environment(\.colorScheme) var colorScheme
let damus: DamusState let damus: DamusState
let show_friend_icon: Bool let show_friend_icon: Bool
@@ -65,37 +27,66 @@ struct TimelineView: View {
MainContent 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 { var MainContent: some View {
ScrollViewReader { scroller in ScrollViewReader { scroller in
ScrollView { ZStack {
InnerTimelineView(events: loading ? .constant(Constants.EXAMPLE_EVENTS) : $events, damus: damus, show_friend_icon: show_friend_icon, filter: loading ? { _ in true } : filter) VStack {
.redacted(reason: loading ? .placeholder : []) LoadMoreButton(events: events, scroller: scroller)
.shimmer(loading) .padding([.top], 10)
.disabled(loading) Spacer()
} }
.onReceive(NotificationCenter.default.publisher(for: .scroll_to_top)) { _ in .zIndex(10.0)
guard let event = events.filter(self.filter).first else {
return 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 { struct TimelineView_Previews: PreviewProvider {
@StateObject static var events = test_event_holder
static var previews: some View { 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()
}
}