diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ac116396..98364266 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15628,9 +15628,6 @@ } } } - }, - "Jump to present" : { - }, "Key" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index d05151f5..1ac17a2e 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -62,7 +62,6 @@ BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; }; BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; }; - BCD93CB82D9E0D9F006C9214 /* ScrollToBottomButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD93CB72D9E0D9F006C9214 /* ScrollToBottomButtonView.swift */; }; BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */; }; BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */; }; BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */; }; @@ -327,7 +326,6 @@ BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = ""; }; BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = ""; }; - BCD93CB72D9E0D9F006C9214 /* ScrollToBottomButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButtonView.swift; sourceTree = ""; }; BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = ""; }; BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartNodeIntent.swift; sourceTree = ""; }; BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsProvider.swift; sourceTree = ""; }; @@ -1058,7 +1056,6 @@ DD5E523D298F5A7D00D21B61 /* Weather */, DD6F65712C6AB8EC0053C113 /* SecureInput.swift */, 8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */, - BCD93CB72D9E0D9F006C9214 /* ScrollToBottomButtonView.swift */, ); path = Helpers; sourceTree = ""; @@ -1439,7 +1436,6 @@ B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */, DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, 233E99C32D849D7A00CC3A77 /* WeightCompactWidget.swift in Sources */, - BCD93CB82D9E0D9F006C9214 /* ScrollToBottomButtonView.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, diff --git a/Meshtastic/Views/Helpers/ScrollToBottomButtonView.swift b/Meshtastic/Views/Helpers/ScrollToBottomButtonView.swift deleted file mode 100644 index 50ddee24..00000000 --- a/Meshtastic/Views/Helpers/ScrollToBottomButtonView.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// ScrollToBottomButtonView.swift -// Meshtastic -// -// Created by Benjamin Faershtein on 4/2/25. -// - -import SwiftUI - -struct ScrollToBottomButtonView: View { - var body: some View { - HStack(spacing: 4) { - Text("Jump to present") - .font(.caption) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .cornerRadius(12) - Image(systemName: "arrow.down") - .font(.title2) - .symbolRenderingMode(.hierarchical) - - } - .foregroundColor(.accentColor) - .shadow(radius: 2) - } -} - -#Preview { - ScrollToBottomButtonView() -} diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 8c03572a..4480b53b 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -22,191 +22,128 @@ struct ChannelMessageList: View { @ObservedObject var channel: ChannelEntity @State private var replyMessageId: Int64 = 0 @AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1 - - // Scroll state - @State private var showScrollToBottomButton = false - @State private var hasReachedBottom = false - @State private var gotFirstUnreadMessage: Bool = false var body: some View { VStack { ScrollViewReader { scrollView in - ZStack(alignment: .bottomTrailing) { - ScrollView { - LazyVStack { - ForEach(channel.allPrivateMessages) { (message: MessageEntity) in - let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false) - if message.replyID > 0 { - let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID }) + ScrollView { + LazyVStack { + ForEach( channel.allPrivateMessages ) { (message: MessageEntity) in + let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false) + if message.replyID > 0 { + let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID }) + HStack { + Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2) + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.blue, lineWidth: 0.5) + ) + Image(systemName: "arrowshape.turn.up.left.fill") + .symbolRenderingMode(.hierarchical) + .imageScale(.large).foregroundColor(.accentColor) + .padding(.trailing) + } + } + HStack(alignment: .bottom) { + if currentUser { Spacer(minLength: 50) } + if !currentUser { + CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44, node: getNodeInfo(id: Int64(message.fromUser?.num ?? 0), context: context)) + .padding(.all, 5) + .offset(y: -7) + } + + VStack(alignment: currentUser ? .trailing : .leading) { + let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) + + if !currentUser && message.fromUser != nil { + Text("\(message.fromUser?.longName ?? "unknown".localized ) (\(message.fromUser?.userId ?? "?"))") + .font(.caption) + .foregroundColor(.gray) + .offset(y: 8) + } + HStack { - Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2) - .padding(10) - .overlay( - RoundedRectangle(cornerRadius: 18) - .stroke(Color.blue, lineWidth: 0.5) - ) - Image(systemName: "arrowshape.turn.up.left.fill") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.accentColor) - .padding(.trailing) + MessageText( + message: message, + tapBackDestination: .channel(channel), + isCurrentUser: currentUser + ) { + self.replyMessageId = message.messageId + self.messageFieldFocused = true + } + + if currentUser && message.canRetry { + RetryButton(message: message, destination: .channel(channel)) + } + } + + TapbackResponses(message: message) { + appState.unreadChannelMessages = myInfo.unreadMessages + context.refresh(myInfo, mergeChanges: true) + } + + HStack { + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + if currentUser && message.receivedACK { + Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) + .foregroundStyle(ackErrorVal?.color ?? Color.red) + .font(.caption2) + } else if currentUser && message.ackError == 0 { + // Empty Error + Text("Waiting to be acknowledged. . .").font( + .caption2) + .foregroundColor(.orange) + } else if currentUser && !isDetectionSensorMessage { + Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) + .foregroundStyle(ackErrorVal?.color ?? Color.red) + .font(.caption2) + } } } - HStack(alignment: .bottom) { - if currentUser { Spacer(minLength: 50) } - if !currentUser { - CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44, node: getNodeInfo(id: Int64(message.fromUser?.num ?? 0), context: context)) - .padding(.all, 5) - .offset(y: -7) - } + .padding(.bottom) + .id(channel.allPrivateMessages.firstIndex(of: message)) + + if !currentUser { + Spacer(minLength: 50) } - HStack(alignment: .bottom) { - if currentUser { Spacer(minLength: 50) } - if !currentUser { - CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44) - .padding(.all, 5) - .offset(y: -7) - } - - VStack(alignment: currentUser ? .trailing : .leading) { - let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) - - if !currentUser && message.fromUser != nil { - Text("\(message.fromUser?.longName ?? "unknown".localized ) (\(message.fromUser?.userId ?? "?"))") - .font(.caption) - .foregroundColor(.gray) - .offset(y: 8) - } - - HStack { - MessageText( - message: message, - tapBackDestination: .channel(channel), - isCurrentUser: currentUser - ) { - self.replyMessageId = message.messageId - self.messageFieldFocused = true - } - - if currentUser && message.canRetry { - RetryButton(message: message, destination: .channel(channel)) - } - } - - TapbackResponses(message: message) { - appState.unreadChannelMessages = myInfo.unreadMessages - context.refresh(myInfo, mergeChanges: true) - } - - HStack { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) - if currentUser && message.receivedACK { - Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - .foregroundStyle(ackErrorVal?.color ?? Color.red) - .font(.caption2) - } else if currentUser && message.ackError == 0 { - // Empty Error - Text("Waiting to be acknowledged. . .").font( - .caption2) - .foregroundColor(.orange) - } else if currentUser && !isDetectionSensorMessage { - Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - .foregroundStyle(ackErrorVal?.color ?? Color.red) - .font(.caption2) - } - } - } - .padding(.bottom) - .id(channel.allPrivateMessages.firstIndex(of: message)) - - if !currentUser { - Spacer(minLength: 50) - } - } - .padding([.leading, .trailing]) - .frame(maxWidth: .infinity) - .id(message.messageId) - .onAppear { - if gotFirstUnreadMessage{ - if !message.read { - message.read = true - do { - for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) { - unreadMessage.read = true - } - try context.save() - Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ") - appState.unreadChannelMessages = myInfo.unreadMessages - context.refresh(myInfo, mergeChanges: true) - } catch { - Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)") - } - } - // Check if we've reached the bottom message - if message.messageId == channel.allPrivateMessages.last?.messageId { - hasReachedBottom = true - showScrollToBottomButton = false + } + .padding([.leading, .trailing]) + .frame(maxWidth: .infinity) + .id(message.messageId) + .onAppear { + if !message.read { + message.read = true + do { + for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) { + unreadMessage.read = true } + try context.save() + Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ") + appState.unreadChannelMessages = myInfo.unreadMessages + context.refresh(myInfo, mergeChanges: true) + } catch { + Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)") } } } - // Invisible spacer to detect reaching bottom - Color.clear - .frame(height: 1) - .id("bottomAnchor") - .onAppear { - hasReachedBottom = true - showScrollToBottomButton = false - } } } - .scrollDismissesKeyboard(.interactively) - .onFirstAppear { - // Find first unread message - if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId { - withAnimation { - scrollView.scrollTo(firstUnreadMessageId, anchor: .top) - showScrollToBottomButton = true - } - } else { - // If no unread messages, scroll to bottom - withAnimation { - scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) - hasReachedBottom = true - } - } - gotFirstUnreadMessage = true + } + .scrollDismissesKeyboard(.interactively) + .onFirstAppear { + withAnimation { + scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) } - .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in - withAnimation { - scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) - hasReachedBottom = true - showScrollToBottomButton = false - } + } + .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in + withAnimation { + scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) } - .onChange(of: channel.allPrivateMessages) { - if hasReachedBottom { - withAnimation { - scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) - } - } else { - showScrollToBottomButton = true - } - } - - // Scroll to bottom button - if showScrollToBottomButton { - Button { - withAnimation { - scrollView.scrollTo("bottomAnchor", anchor: .bottom) - hasReachedBottom = true - showScrollToBottomButton = false - } - } label: { - ScrollToBottomButtonView() - } - .padding(.bottom, 8) - .padding(.trailing, 16) - .transition(.opacity) + } + .onChange(of: channel.allPrivateMessages) { + withAnimation { + scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) } } } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 6f995756..dea4586f 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -20,172 +20,115 @@ struct UserMessageList: View { // View State Items @ObservedObject var user: UserEntity @State private var replyMessageId: Int64 = 0 - - // Scroll state - @State private var showScrollToBottomButton = false - @State private var hasReachedBottom = false - @State private var gotFirstUnreadMessage: Bool = false var body: some View { VStack { ScrollViewReader { scrollView in - ZStack(alignment: .bottomTrailing) { - ScrollView { - LazyVStack { - ForEach( user.messageList ) { (message: MessageEntity) in - if user.num != bleManager.connectedPeripheral?.num ?? -1 { - let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false) + ScrollView { + LazyVStack { + ForEach( user.messageList ) { (message: MessageEntity) in + if user.num != bleManager.connectedPeripheral?.num ?? -1 { + let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false) - if message.replyID > 0 { - let messageReply = user.messageList.first(where: { $0.messageId == message.replyID }) + if message.replyID > 0 { + let messageReply = user.messageList.first(where: { $0.messageId == message.replyID }) + HStack { + Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2) + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.blue, lineWidth: 0.5) + ) + Image(systemName: "arrowshape.turn.up.left.fill") + .symbolRenderingMode(.hierarchical) + .imageScale(.large).foregroundColor(.accentColor) + .padding(.trailing) + } + } + HStack(alignment: .top) { + if currentUser { Spacer(minLength: 50) } + VStack(alignment: currentUser ? .trailing : .leading) { HStack { - Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2) - .padding(10) - .overlay( - RoundedRectangle(cornerRadius: 18) - .stroke(Color.blue, lineWidth: 0.5) - ) - Image(systemName: "arrowshape.turn.up.left.fill") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.accentColor) - .padding(.trailing) + MessageText( + message: message, + tapBackDestination: .user(user), + isCurrentUser: currentUser + ) { + self.replyMessageId = message.messageId + self.messageFieldFocused = true + } + + if currentUser && message.canRetry || (message.receivedACK && !message.realACK) { + RetryButton(message: message, destination: .user(user)) + } } - } - HStack(alignment: .top) { - if currentUser { Spacer(minLength: 50) } - VStack(alignment: currentUser ? .trailing : .leading) { - HStack { - MessageText( - message: message, - tapBackDestination: .user(user), - isCurrentUser: currentUser - ) { - self.replyMessageId = message.messageId - self.messageFieldFocused = true - } - if currentUser && message.canRetry || (message.receivedACK && !message.realACK) { - RetryButton(message: message, destination: .user(user)) - } - } + TapbackResponses(message: message) { + appState.unreadDirectMessages = user.unreadMessages + } - TapbackResponses(message: message) { - appState.unreadDirectMessages = user.unreadMessages - } - - HStack { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) - if currentUser && message.receivedACK { - // Ack Received - if message.realACK { - Text("\(ackErrorVal?.display ?? "Empty Ack Error")") - .font(.caption2) - .foregroundStyle(ackErrorVal?.color ?? Color.secondary) - } else { - Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange) - } - } else if currentUser && message.ackError == 0 { - // Empty Error - Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.yellow) - } else if currentUser && message.ackError > 0 { - Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - .foregroundStyle(ackErrorVal?.color ?? Color.red) + HStack { + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + if currentUser && message.receivedACK { + // Ack Received + if message.realACK { + Text("\(ackErrorVal?.display ?? "Empty Ack Error")") .font(.caption2) + .foregroundStyle(ackErrorVal?.color ?? Color.secondary) + } else { + Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange) } + } else if currentUser && message.ackError == 0 { + // Empty Error + Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.yellow) + } else if currentUser && message.ackError > 0 { + Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) + .foregroundStyle(ackErrorVal?.color ?? Color.red) + .font(.caption2) } } - .padding(.bottom) - .id(user.messageList.firstIndex(of: message)) + } + .padding(.bottom) + .id(user.messageList.firstIndex(of: message)) - if !currentUser { - Spacer(minLength: 50) - } - } - .padding([.leading, .trailing]) - .frame(maxWidth: .infinity) - .id(message.messageId) - .onAppear { - if gotFirstUnreadMessage { - if !message.read { - message.read = true - do { - for unreadMessage in user.messageList.filter({ !$0.read }) { - unreadMessage.read = true - } - try context.save() - Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ") - appState.unreadDirectMessages = user.unreadMessages - } catch { - Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)") - } - } - // Check if we've reached the bottom message - if message.messageId == user.messageList.last?.messageId { - hasReachedBottom = true - showScrollToBottomButton = false - } - } + if !currentUser { + Spacer(minLength: 50) } } - } - // Invisible spacer to detect reaching bottom - Color.clear - .frame(height: 1) - .id("bottomAnchor") + .padding([.leading, .trailing]) + .frame(maxWidth: .infinity) + .id(message.messageId) .onAppear { - hasReachedBottom = true - showScrollToBottomButton = false + if !message.read { + message.read = true + do { + try context.save() + Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ") + appState.unreadDirectMessages = user.unreadMessages + + } catch { + Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)") + } + } } - } - } - .scrollDismissesKeyboard(.interactively) - .onFirstAppear { - // Find first unread message - if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId { - withAnimation { - scrollView.scrollTo(firstUnreadMessageId, anchor: .top) - showScrollToBottomButton = true - } - } else { - // If no unread messages, scroll to bottom - withAnimation { - scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) - hasReachedBottom = true } } - gotFirstUnreadMessage = true } - .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in - withAnimation { - scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) - hasReachedBottom = true - showScrollToBottomButton = false - } + } + .scrollDismissesKeyboard(.interactively) + .onFirstAppear { + withAnimation { + scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) } - .onChange(of: user.messageList) { - if hasReachedBottom { - withAnimation { - scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) - } - } else { - showScrollToBottomButton = true - } + } + .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in + withAnimation { + scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) } - - // Scroll to bottom button - if showScrollToBottomButton { - Button { - withAnimation { - scrollView.scrollTo("bottomAnchor", anchor: .bottom) - hasReachedBottom = true - showScrollToBottomButton = false - } - } label: { - ScrollToBottomButtonView() - } - .padding(.bottom, 8) - .padding(.trailing, 16) - .transition(.opacity) + } + .onChange(of: user.messageList) { + withAnimation { + scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) } } }