diff --git a/Meshtastic/Router/Router.swift b/Meshtastic/Router/Router.swift index 9eae6c9b..45b2f569 100644 --- a/Meshtastic/Router/Router.swift +++ b/Meshtastic/Router/Router.swift @@ -1,3 +1,4 @@ +import Combine import CoreData import OSLog import SwiftUI @@ -8,10 +9,16 @@ class Router: ObservableObject { @Published var navigationState: NavigationState + private var cancellables: Set = [] + init( navigationState: NavigationState = .bluetooth ) { self.navigationState = navigationState + + $navigationState.sink { destination in + Logger.services.info("Routed to \(String(describing: destination))") + }.store(in: &cancellables) } func route(to destination: NavigationState) { diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index cb7f1894..a4fd86bf 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -14,9 +14,12 @@ struct ChannelList: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @State var node: NodeInfoEntity? + @Binding + var node: NodeInfoEntity? + + @Binding + var channelSelection: ChannelEntity? - @State private var channelSelection: ChannelEntity? // Nothing selected by default. @State private var isPresentingDeleteChannelMessagesConfirm: Bool = false @State private var isPresentingTraceRouteSentAlert = false @@ -24,14 +27,14 @@ struct ChannelList: View { var restrictedChannels = ["gpio", "mqtt", "serial"] @ViewBuilder - private func makeNavigationLink( + private func makeChannelRow( myInfo: MyInfoEntity, channel: ChannelEntity ) -> some View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY") - NavigationLink(destination: ChannelMessageList(myInfo: myInfo, channel: channel)) { + NavigationLink(value: channel) { let mostRecent = channel.allPrivateMessages.last(where: { $0.channel == channel.index }) let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 @@ -101,51 +104,50 @@ struct ChannelList: View { VStack { // Display Contacts for the rest of the non admin channels if let node, let myInfo = node.myInfo, let channels = myInfo.channels?.array as? [ChannelEntity] { - List(channels, id: \.self, selection: $channelSelection) { (channel: ChannelEntity) in - if !restrictedChannels.contains(channel.name?.lowercased() ?? "") { - makeNavigationLink(myInfo: myInfo, channel: channel) - .frame(height: 62) - .contextMenu { - if channel.allPrivateMessages.count > 0 { - Button(role: .destructive) { - isPresentingDeleteChannelMessagesConfirm = true - channelSelection = channel - } label: { - Label("Delete Messages", systemImage: "trash") - } - } - Button { - channel.mute = !channel.mute - - do { - let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!) - if adminMessageId > 0 { - context.refresh(channel, mergeChanges: true) + List(selection: $channelSelection) { + ForEach(channels) { (channel: ChannelEntity) in + if !restrictedChannels.contains(channel.name?.lowercased() ?? "") { + makeChannelRow(myInfo: myInfo, channel: channel) + .frame(height: 62) + .contextMenu { + if channel.allPrivateMessages.count > 0 { + Button(role: .destructive) { + isPresentingDeleteChannelMessagesConfirm = true + channelSelection = channel + } label: { + Label("Delete Messages", systemImage: "trash") } - - try context.save() - - } catch { - context.rollback() - Logger.data.error("💥 Save Channel Mute Error") } - } label: { - Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash") + Button { + channel.mute = !channel.mute + do { + let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!) + if adminMessageId > 0 { + context.refresh(channel, mergeChanges: true) + } + try context.save() + } catch { + context.rollback() + Logger.data.error("💥 Save Channel Mute Error") + } + } label: { + Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash") + } } - } - .confirmationDialog( - "This conversation will be deleted.", - isPresented: $isPresentingDeleteChannelMessagesConfirm, - titleVisibility: .visible - ) { - Button(role: .destructive) { - deleteChannelMessages(channel: channelSelection!, context: context) - context.refresh(myInfo, mergeChanges: true) - channelSelection = nil - } label: { - Text("delete") + .confirmationDialog( + "This conversation will be deleted.", + isPresented: $isPresentingDeleteChannelMessagesConfirm, + titleVisibility: .visible + ) { + Button(role: .destructive) { + deleteChannelMessages(channel: channelSelection!, context: context) + context.refresh(myInfo, mergeChanges: true) + channelSelection = nil + } label: { + Text("delete") + } } - } + } } } .padding([.top, .bottom]) diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index b4a86047..0d1cdab8 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -25,7 +25,7 @@ struct Messages: View { @Binding var unreadDirectMessages: Int - + // Aliases the navigation state for the NavigationSplitView sidebar selection private var messagesSelection: Binding { Binding( @@ -88,28 +88,50 @@ struct Messages: View { .navigationBarTitleDisplayMode(.large) .navigationBarItems(leading: MeshtasticLogo()) .onAppear { - let nodeId = Int64(UserDefaults.preferredPeripheralNum) - if nodeId > 0 { - node = getNodeInfo(id: nodeId, context: context) - } + setupNavigationState() } } content: { - if case .messages(let state) = router.navigationState { - switch state { - case .channels: - // TODO: support linking to the channel - ChannelList(node: node) - case .directMessages(userNum: let userNum, messageId: _): - UserList( - node: node, - selectedUserNum: userNum - ) - default: - EmptyView() - } + if case .messages(.channels) = router.navigationState { + ChannelList(node: $node, channelSelection: $channelSelection) + } else if case .messages(.directMessages) = router.navigationState { + UserList(node: $node, userSelection: $userSelection) } } detail: { + if let myInfo = node?.myInfo, let channelSelection { + ChannelMessageList(myInfo: myInfo, channel: channelSelection) + } else if let userSelection { + UserMessageList(user: userSelection) + } + } + } + private func setupNavigationState() { + let nodeId = Int64(UserDefaults.preferredPeripheralNum) + if nodeId > 0 { + node = getNodeInfo(id: nodeId, context: context) + } + + guard case .messages(let state) = router.navigationState else { + return + } + + if let state { + switch state { + case .channels(channelId: let channelId, messageId: _): + if let channelId { + channelSelection = node?.myInfo?.channels?.first(where: { channel in + guard let channel = channel as? ChannelEntity else { return false } + return channel.id == channelId + }) as? ChannelEntity + } + case .directMessages(userNum: let userNum, messageId: _): + if let userNum { + userSelection = getUser(id: userNum, context: context) + } + } + } else { + channelSelection = nil + userSelection = nil } } } diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 7e3267a5..5c7214fe 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -33,19 +33,20 @@ struct UserList: View { sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), NSSortDescriptor(key: "userNode.favorite", ascending: false), NSSortDescriptor(key: "longName", ascending: true)], - animation: .default) - + animation: .default + ) private var users: FetchedResults - @State var node: NodeInfoEntity? - @State var selectedUserNum: Int64? - @State private var userSelection: UserEntity? // Nothing selected by default. + + @Binding var node: NodeInfoEntity? + @Binding var userSelection: UserEntity? + @State private var isPresentingDeleteUserMessagesConfirm: Bool = false var body: some View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY") VStack { - List { + List(selection: $userSelection) { if #available(iOS 17.0, macOS 14.0, *) { TipView(ContactsTip(), arrowEdge: .bottom) } @@ -54,8 +55,8 @@ struct UserList: View { let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 - if user.num != bleManager.connectedPeripheral?.num ?? 0 { - NavigationLink(destination: UserMessageList(user: user)) { + if user.num != bleManager.connectedPeripheral?.num ?? 0 { + NavigationLink(value: user) { ZStack { Image(systemName: "circle.fill") .opacity(user.unreadMessages > 0 ? 1 : 0) @@ -205,9 +206,6 @@ struct UserList: View { .onChange(of: distanceFilter) { _ in searchUserList() } - .onChange(of: selectedUserNum) { newUserNum in - userSelection = users.first(where: { $0.num == newUserNum }) - } .onAppear { searchUserList() }