mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/6141f5ab-6bd5-43f5-9922-a7fde05f0d5d Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
156 lines
5 KiB
Swift
156 lines
5 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
|
|
@Environment(\.scenePhase) var scenePhase
|
|
@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)")
|
|
}
|
|
}
|
|
|
|
private func routerIsShowingThisChannel() -> Bool {
|
|
guard appState.router.selectedTab == .messages else { return false }
|
|
return scenePhase == .active
|
|
}
|
|
|
|
var body: some View {
|
|
// Cast allPrivateMessages 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
|
|
}()
|
|
|
|
ScrollViewReader { scrollView in
|
|
ScrollView {
|
|
LazyVStack {
|
|
ForEach(messages, id: \.messageId) { message in
|
|
let previousMessage: MessageEntity? = previousByID[message.messageId] ?? 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
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
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|