Meshtastic-Apple/Meshtastic/Views/Messages/UserMessageList.swift
Garth Vander Houwen 8f9be79c55
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>
2025-10-10 14:07:36 -07:00

147 lines
4.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 accessoryManager: AccessoryManager
@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
private var allPrivateMessages: [MessageEntity] {
// Cast user.messageList to an array for easier indexing and ForEach.
return user.messageList.compactMap { $0 as MessageEntity }
}
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).")
appState.unreadDirectMessages = user.unreadMessages
context.refresh(user, mergeChanges: true)
} catch {
Logger.data.error("Failed to read direct messages: \(error.localizedDescription, privacy: .public)")
}
}
var body: some View {
VStack {
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach(allPrivateMessages.indices, id: \.self) { index in
let message = allPrivateMessages[index]
let previousMessage = index > 0 ? allPrivateMessages[index - 1] : nil
UserMessageRow(
message: message,
allMessages: allPrivateMessages,
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)
}
}
}
.id(redrawTapbacksTrigger)
}
// 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 ?? "?")
}
}
}
}
}