Meshtastic-Apple/Meshtastic/Views/Messages/UserMessageList.swift
Garth Vander Houwen 69c318a9e1
Message list performance fixes into 2.7.6 (#1475)
* Remove extra want config call when adding a contact

* App badge and unnecessary notification fixes (#1455)

* - Fix for app badge not going to zero if a message arrives while you have that chat open
- Fix for push notifications popping up when a message is received while that chat is open

* Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message

* Fix: Channels help grammer fix (#1452)

* remove outdated TCP not available on Apple devices (#1450)

* Update initial onboarding view

* remove toggle gating for mac

* Update crash reporting opt out in real time

* Update onboarding text

* Use mDNS text records for node name

* TCP IP and port on the connection screen

* Hide app icon chooser on mac

* Infinite loop hang bugfixes and performance improvements for both `UserMessageList` and `ChannelMessageList` (#1465)

* 2.7.5 Working Changes (#1460)

* Remove extra want config call when adding a contact

* App badge and unnecessary notification fixes (#1455)

* - Fix for app badge not going to zero if a message arrives while you have that chat open
- Fix for push notifications popping up when a message is received while that chat is open

* Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message

* Fix: Channels help grammer fix (#1452)

* remove outdated TCP not available on Apple devices (#1450)

* Update initial onboarding view

* remove toggle gating for mac

* Update crash reporting opt out in real time

* Update onboarding text

---------

Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com>
Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com>
Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com>

* UserEntity: add mostRecentMessage and unreadMessages with early exit when lastMessage is nil, and fetch 1 row (not N) otherwise

* UserList: replace 5 slow calls to user.messageList with new fast calls

* NodeList: always put the connected node at the top of list (if it matches the node filters)

* ChannelEntity: add faster mostRecentPrivateMessage and unreadMessages which fetch 1 row (not N)

* ChannelList: replace 5 calls to channel.allPrivateMessage with new fast calls

* Fix incorrect appState.unreadDirectMessages calculations

* MyInfoEntity: also fix unreadMessages count here to be fast, and use it for appState.unreadChannelMessages

* UserMessageList: use @FetchRequest to prevent the N^2 behavior that was happening in calls to allPrivateMessages

* Refactor ChannelEntityExtension and MyInfoEntityExtension to be more similar to UserEntityExtension

* Remove SwiftUI-infinite-loop-causing `.id(redrawTapbacksTrigger)` in ChannelMessageList and UserMessageList (duplicate row ids)

* MyInfoEntityExtension: exclude emoji tapbacks (which never get marked as read anyway) from unread message count

* Add SaveChannelLinkData so MessageText and MeshtasticApp can use .sheet(item: ...) and avoid infinite loop hang due to Binding rebuild

* ChannelMessageList and UserMessageList: switch to stable messageId for ForEach SwiftUI row identity

* ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification

* ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear

* ChannelMessageList and UserMessageList: block spurious markMessagesAsRead when this View is not active

---------

Co-authored-by: Garth Vander Houwen <garth@meshtastic.com>
Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com>
Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com>
Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com>

* message-list-performance: revert scrolling changes (#1472)

* Revert e0f0b4a0f7 (ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear)

* Revert "ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification"

This reverts commit ee1a7c4415.

---------

Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com>
Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com>
Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com>
Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu>
2025-10-17 18:16:00 -07:00

173 lines
5.5 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
//  UserMessageList.swift
//  MeshtasticApple
//
//  Created by Garth Vander Houwen on 12/24/21.
//
import SwiftUI
import CoreData
import OSLog
import MeshtasticProtobufs // Added to ensure RoutingError is accessible if needed
struct UserMessageList: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var router: Router
@EnvironmentObject var accessoryManager: AccessoryManager
@Environment(\.scenePhase) var scenePhase
@Environment(\.managedObjectContext) var context
@FocusState var messageFieldFocused: Bool
@ObservedObject var user: UserEntity
@State private var replyMessageId: Int64 = 0
@State private var messageToHighlight: Int64 = 0
@State private var redrawTapbacksTrigger = UUID()
@AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1
@FetchRequest private var allPrivateMessages: FetchedResults<MessageEntity>
init(user: UserEntity) {
self.user = user
// Configure fetch request here
let request: NSFetchRequest<MessageEntity> = user.messageFetchRequest
_allPrivateMessages = FetchRequest(fetchRequest: request)
}
func handleInteractionComplete() {
markMessagesAsRead()
redrawTapbacksTrigger = UUID()
}
func markMessagesAsRead() {
do {
for unreadMessage in allPrivateMessages.filter({ !$0.read }) {
unreadMessage.read = true
}
try context.save()
Logger.data.info("📖 [App] All unread direct messages marked as read for user \(user.num, privacy: .public).")
if let connectedPeripheralNum = accessoryManager.activeDeviceNum,
let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: context),
let connectedUser = connectedNode.user {
appState.unreadDirectMessages = connectedUser.unreadMessages(context: context, skipLastMessageCheck: true) // skipLastMessageCheck=true because we don't update lastMessage on our own connected node
}
context.refresh(user, mergeChanges: true)
} catch {
Logger.data.error("Failed to read direct messages: \(error.localizedDescription, privacy: .public)")
}
}
private func routerIsShowingThisUser() -> Bool {
guard router.navigationState.selectedTab == .messages else { return false }
return scenePhase == .active
}
var body: some View {
// Cast user.messageList to an array for easier indexing and ForEach.
let messages: [MessageEntity] = Array(allPrivateMessages)
// Precompute previous message
let previousByID: [Int64: MessageEntity?] = {
var dict = [Int64: MessageEntity?]()
var prev: MessageEntity?
for m in messages { dict[m.messageId] = prev; prev = m }
return dict
}()
VStack {
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach(messages, id: \.messageId) { message in
let previousMessage: MessageEntity? = previousByID[message.messageId] ?? nil
UserMessageRow(
message: message,
allMessages: messages,
previousMessage: previousMessage,
preferredPeripheralNum: preferredPeripheralNum,
user: user,
replyMessageId: $replyMessageId,
messageFieldFocused: $messageFieldFocused,
messageToHighlight: $messageToHighlight,
scrollView: scrollView,
onInteractionComplete: handleInteractionComplete
)
.onAppear {
// Only mark as read if the app is in the foreground
if !message.read && UIApplication.shared.applicationState == .active {
message.read = true
LocalNotificationManager().cancelNotificationForMessageId(message.messageId)
// Race condition, sometimes the app doesn't update unread count if we run this too early
// So, run it in the main queue after everything saves and stabilizes
DispatchQueue.main.async {
markMessagesAsRead()
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
}
}
}
}
// Invisible spacer to detect reaching bottom
Color.clear
.frame(height: 1)
.id("bottomAnchor")
}
}
.defaultScrollAnchor(.bottom)
.defaultScrollAnchorTopAlignment()
.defaultScrollAnchorBottomSizeChanges()
.scrollDismissesKeyboard(.immediately)
.onChange(of: messageFieldFocused) {
if messageFieldFocused {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
}
}
}
}
TextMessageField(
destination: .user(user),
replyMessageId: $replyMessageId,
isFocused: $messageFieldFocused
)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if !user.keyMatch {
ToolbarItem(placement: .bottomBar) {
VStack {
HStack {
Image(systemName: "key.slash.fill")
.symbolRenderingMode(.multicolor)
.foregroundStyle(.red)
.font(.caption2)
Text("There is an issue with this contact's public key.")
.foregroundStyle(.secondary)
.font(.caption2)
}
Link(destination: URL(string: "meshtastic:///nodes?nodenum=\(user.num)")!) {
Text("Details...")
.font(.caption2)
.offset(y: -15)
}
}
.offset(y: -15)
}
}
ToolbarItem(placement: .principal) {
HStack {
CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num))), circleSize: 44)
Text(user.longName ?? "Unknown").font(.headline)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
ZStack {
ConnectedDevice(
deviceConnected: accessoryManager.isConnected,
name: accessoryManager.activeConnection?.device.shortName ?? "?")
}
}
}
}
}