Meshtastic-Apple/Meshtastic/Views/Messages/ChannelMessageList.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

141 lines
4.6 KiB
Swift

//
// ChannelMessageList.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 12/24/21.
//
import CoreData
import MeshtasticProtobufs
import OSLog
import SwiftUI
struct ChannelMessageList: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var router: Router
@Environment(\.managedObjectContext) var context
@EnvironmentObject var accessoryManager: AccessoryManager
@FocusState var messageFieldFocused: Bool
@ObservedObject var myInfo: MyInfoEntity
@ObservedObject var channel: ChannelEntity
@State private var replyMessageId: Int64 = 0
@State private var redrawTapbacksTrigger = UUID()
@AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1
@State private var messageToHighlight: Int64 = 0
@FetchRequest private var allPrivateMessages: FetchedResults<MessageEntity>
init(myInfo: MyInfoEntity, channel: ChannelEntity) {
self.myInfo = myInfo
self.channel = channel
// Configure fetch request here
let request: NSFetchRequest<MessageEntity> = MessageEntity.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(keyPath: \MessageEntity.messageTimestamp, ascending: true)
]
request.predicate = NSPredicate(
format: "channel == %ld AND toUser == nil AND isEmoji == false",
channel.index
)
_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 messages marked as read.")
appState.unreadChannelMessages = myInfo.unreadMessages
context.refresh(myInfo, mergeChanges: true)
} catch {
Logger.data.error("Failed to read messages: \(error.localizedDescription, privacy: .public)")
}
}
var body: some View {
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach(allPrivateMessages.indices, id: \.self) { index in
let message = allPrivateMessages[index]
let previousMessage = index > 0 ? allPrivateMessages[index - 1] : nil
ChannelMessageRow(
message: message,
allMessages: allPrivateMessages,
previousMessage: previousMessage,
preferredPeripheralNum: preferredPeripheralNum,
channel: channel,
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)
}
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: .channel(channel),
replyMessageId: $replyMessageId,
isFocused: $messageFieldFocused
)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
HStack {
CircleText(text: String(channel.index), color: .accentColor, circleSize: 44).fixedSize()
Text(String(channel.name ?? "Unknown").camelCaseToWords()).font(.headline)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
ZStack {
ConnectedDevice(
deviceConnected: accessoryManager.isConnected,
name: accessoryManager.activeConnection?.device.shortName ?? "?",
mqttProxyConnected: accessoryManager.mqttProxyConnected && (channel.uplinkEnabled || channel.downlinkEnabled),
mqttUplinkEnabled: channel.uplinkEnabled,
mqttDownlinkEnabled: channel.downlinkEnabled,
mqttTopic: accessoryManager.mqttManager.topic
)
}
}
}
}
}