mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 05:05:19 +00:00
ux(chat): scroll to bottom on geohash open; add infinite scroll up with stable anchor; track window size per chat
This commit is contained in:
@@ -80,6 +80,9 @@ struct ContentView: View {
|
||||
@State private var autocompleteDebounceTimer: Timer?
|
||||
@State private var showLocationChannelsSheet = false
|
||||
@State private var expandedMessageIDs: Set<String> = []
|
||||
// Window sizes for rendering (infinite scroll up)
|
||||
@State private var windowCountPublic: Int = 300
|
||||
@State private var windowCountPrivate: [String: Int] = [:]
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
@@ -295,8 +298,12 @@ struct ContentView: View {
|
||||
}
|
||||
}()
|
||||
|
||||
// Implement windowing - show last 300 messages for performance
|
||||
let windowedMessages = messages.suffix(300)
|
||||
// Implement windowing with adjustable window count per chat
|
||||
let currentWindowCount: Int = {
|
||||
if let peer = privatePeer { return windowCountPrivate[peer] ?? 300 }
|
||||
return windowCountPublic
|
||||
}()
|
||||
let windowedMessages = messages.suffix(currentWindowCount)
|
||||
|
||||
// Build stable UI IDs with a context key to avoid ID collisions when switching channels
|
||||
#if os(iOS)
|
||||
@@ -413,6 +420,40 @@ struct ContentView: View {
|
||||
if message.id == windowedMessages.last?.id {
|
||||
isAtBottom.wrappedValue = true
|
||||
}
|
||||
// Infinite scroll up: when top row appears, increase window and preserve anchor
|
||||
if message.id == windowedMessages.first?.id, messages.count > windowedMessages.count {
|
||||
let step = 200
|
||||
#if os(iOS)
|
||||
let contextKey: String = {
|
||||
switch locationManager.selectedChannel {
|
||||
case .mesh: return "mesh"
|
||||
case .location(let ch): return "geo:\(ch.geohash)"
|
||||
}
|
||||
}()
|
||||
#else
|
||||
let contextKey: String = "mesh"
|
||||
#endif
|
||||
let preserveID = "\(contextKey)|\(message.id)"
|
||||
if let peer = privatePeer {
|
||||
let current = windowCountPrivate[peer] ?? 300
|
||||
let newCount = min(messages.count, current + step)
|
||||
if newCount != current {
|
||||
windowCountPrivate[peer] = newCount
|
||||
DispatchQueue.main.async {
|
||||
proxy.scrollTo(preserveID, anchor: .top)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let current = windowCountPublic
|
||||
let newCount = min(messages.count, current + step)
|
||||
if newCount != current {
|
||||
windowCountPublic = newCount
|
||||
DispatchQueue.main.async {
|
||||
proxy.scrollTo(preserveID, anchor: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if message.id == windowedMessages.last?.id {
|
||||
@@ -549,7 +590,8 @@ struct ContentView: View {
|
||||
#else
|
||||
let contextKey: String = "mesh"
|
||||
#endif
|
||||
let target = viewModel.messages.suffix(300).last.map { "\(contextKey)|\($0.id)" }
|
||||
let count = windowCountPublic
|
||||
let target = viewModel.messages.suffix(count).last.map { "\(contextKey)|\($0.id)" }
|
||||
DispatchQueue.main.async {
|
||||
if let target = target { proxy.scrollTo(target, anchor: .bottom) }
|
||||
}
|
||||
@@ -568,7 +610,8 @@ struct ContentView: View {
|
||||
#else
|
||||
let contextKey: String = "mesh"
|
||||
#endif
|
||||
let target = viewModel.messages.suffix(300).last.map { "\(contextKey)|\($0.id)" }
|
||||
let count = windowCountPublic
|
||||
let target = viewModel.messages.suffix(count).last.map { "\(contextKey)|\($0.id)" }
|
||||
DispatchQueue.main.async {
|
||||
if let target = target { proxy.scrollTo(target, anchor: .bottom) }
|
||||
}
|
||||
@@ -594,7 +637,8 @@ struct ContentView: View {
|
||||
if now.timeIntervalSince(lastScrollTime) > 0.5 {
|
||||
lastScrollTime = now
|
||||
let contextKey = "dm:\(peerID)"
|
||||
let target = messages.suffix(300).last.map { "\(contextKey)|\($0.id)" }
|
||||
let count = windowCountPrivate[peerID] ?? 300
|
||||
let target = messages.suffix(count).last.map { "\(contextKey)|\($0.id)" }
|
||||
DispatchQueue.main.async {
|
||||
if let target = target { proxy.scrollTo(target, anchor: .bottom) }
|
||||
}
|
||||
@@ -603,7 +647,8 @@ struct ContentView: View {
|
||||
scrollThrottleTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
|
||||
lastScrollTime = Date()
|
||||
let contextKey = "dm:\(peerID)"
|
||||
let target = messages.suffix(300).last.map { "\(contextKey)|\($0.id)" }
|
||||
let count = windowCountPrivate[peerID] ?? 300
|
||||
let target = messages.suffix(count).last.map { "\(contextKey)|\($0.id)" }
|
||||
DispatchQueue.main.async {
|
||||
if let target = target { proxy.scrollTo(target, anchor: .bottom) }
|
||||
}
|
||||
@@ -613,19 +658,20 @@ struct ContentView: View {
|
||||
}
|
||||
#if os(iOS)
|
||||
.onChange(of: locationManager.selectedChannel) { newChannel in
|
||||
// When switching to a new geohash channel, scroll to the top of that chat
|
||||
// When switching to a new geohash channel, scroll to the bottom
|
||||
guard privatePeer == nil else { return }
|
||||
switch newChannel {
|
||||
case .mesh:
|
||||
break
|
||||
case .location(let ch):
|
||||
// Compute the top item (first of the current window)
|
||||
// Reset window size
|
||||
windowCountPublic = 300
|
||||
let contextKey = "geo:\(ch.geohash)"
|
||||
let top = viewModel.messages.suffix(300).first?.id
|
||||
let target = top.map { "\(contextKey)|\($0)" }
|
||||
isAtBottom.wrappedValue = false
|
||||
let last = viewModel.messages.suffix(windowCountPublic).last?.id
|
||||
let target = last.map { "\(contextKey)|\($0)" }
|
||||
isAtBottom.wrappedValue = true
|
||||
DispatchQueue.main.async {
|
||||
if let target = target { proxy.scrollTo(target, anchor: .top) }
|
||||
if let target = target { proxy.scrollTo(target, anchor: .bottom) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user