Scroll: move bottom detection to View state and pass Binding to messagesView; autoscroll only when bound is true to prevent jumps when user scrolled up

This commit is contained in:
jack
2025-08-22 13:04:56 +02:00
parent 9d5105a8bf
commit 27e5e2962c
+9 -8
View File
@@ -77,6 +77,8 @@ struct ContentView: View {
@State private var selectedMessageSender: String? @State private var selectedMessageSender: String?
@State private var selectedMessageSenderID: String? @State private var selectedMessageSenderID: String?
@FocusState private var isNicknameFieldFocused: Bool @FocusState private var isNicknameFieldFocused: Bool
@State private var isAtBottomPublic: Bool = true
@State private var isAtBottomPrivate: Bool = true
@State private var lastScrollTime: Date = .distantPast @State private var lastScrollTime: Date = .distantPast
@State private var scrollThrottleTimer: Timer? @State private var scrollThrottleTimer: Timer?
@State private var autocompleteDebounceTimer: Timer? @State private var autocompleteDebounceTimer: Timer?
@@ -273,9 +275,8 @@ struct ContentView: View {
// MARK: - Message List View // MARK: - Message List View
private func messagesView(privatePeer: String?) -> some View { private func messagesView(privatePeer: String?, isAtBottom: Binding<Bool>) -> some View {
ScrollViewReader { proxy in ScrollViewReader { proxy in
@State var isAtBottom: Bool = true
ScrollView { ScrollView {
LazyVStack(alignment: .leading, spacing: 0) { LazyVStack(alignment: .leading, spacing: 0) {
// Extract messages based on context (private or public chat) // Extract messages based on context (private or public chat)
@@ -337,12 +338,12 @@ struct ContentView: View {
.onAppear { .onAppear {
// Track if last item is visible to enable auto-scroll only when near bottom // Track if last item is visible to enable auto-scroll only when near bottom
if message.id == windowedMessages.last?.id { if message.id == windowedMessages.last?.id {
isAtBottom = true isAtBottom.wrappedValue = true
} }
} }
.onDisappear { .onDisappear {
if message.id == windowedMessages.last?.id { if message.id == windowedMessages.last?.id {
isAtBottom = false isAtBottom.wrappedValue = false
} }
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
@@ -379,7 +380,7 @@ struct ContentView: View {
.onChange(of: viewModel.messages.count) { _ in .onChange(of: viewModel.messages.count) { _ in
if privatePeer == nil && !viewModel.messages.isEmpty { if privatePeer == nil && !viewModel.messages.isEmpty {
// Only autoscroll when user is at/near bottom // Only autoscroll when user is at/near bottom
guard isAtBottom else { return } guard isAtBottom.wrappedValue else { return }
// Throttle scroll animations to prevent excessive UI updates // Throttle scroll animations to prevent excessive UI updates
let now = Date() let now = Date()
if now.timeIntervalSince(lastScrollTime) > 0.5 { if now.timeIntervalSince(lastScrollTime) > 0.5 {
@@ -401,7 +402,7 @@ struct ContentView: View {
let messages = viewModel.privateChats[peerID], let messages = viewModel.privateChats[peerID],
!messages.isEmpty { !messages.isEmpty {
// Only autoscroll when user is at/near bottom // Only autoscroll when user is at/near bottom
guard isAtBottom else { return } guard isAtBottom.wrappedValue else { return }
// Same throttling for private chats // Same throttling for private chats
let now = Date() let now = Date()
if now.timeIntervalSince(lastScrollTime) > 0.5 { if now.timeIntervalSince(lastScrollTime) > 0.5 {
@@ -727,7 +728,7 @@ struct ContentView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
mainHeaderView mainHeaderView
Divider() Divider()
messagesView(privatePeer: nil) messagesView(privatePeer: nil, isAtBottom: $isAtBottomPublic)
Divider() Divider()
inputView inputView
} }
@@ -777,7 +778,7 @@ struct ContentView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
privateHeaderView privateHeaderView
Divider() Divider()
messagesView(privatePeer: viewModel.selectedPrivateChatPeer) messagesView(privatePeer: viewModel.selectedPrivateChatPeer, isAtBottom: $isAtBottomPrivate)
Divider() Divider()
inputView inputView
} }