Refactor messages navigation handling for NavigationSplitView

This commit is contained in:
Blake McAnally 2024-07-14 01:34:54 -05:00
parent fbf059be6a
commit 86d362bb17
4 changed files with 103 additions and 74 deletions

View file

@ -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<AnyCancellable> = []
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) {

View file

@ -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])

View file

@ -25,7 +25,7 @@ struct Messages: View {
@Binding
var unreadDirectMessages: Int
// Aliases the navigation state for the NavigationSplitView sidebar selection
private var messagesSelection: Binding<MessagesNavigationState?> {
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
}
}
}

View file

@ -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<UserEntity>
@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()
}