mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #224 from meshtastic/2.0_Alpha_Launch
2.0 alpha launch
This commit is contained in:
commit
e862a863e8
14 changed files with 1160 additions and 839 deletions
|
|
@ -17,7 +17,7 @@
|
|||
DD17E5DE277D49D400010EC2 /* storeforward.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17E5DC277D49D400010EC2 /* storeforward.pb.swift */; };
|
||||
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */; };
|
||||
DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B828CDA93900720036 /* SerialConfigEnums.swift */; };
|
||||
DD1BF2F92776FE2E008C8D2F /* MessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* MessageList.swift */; };
|
||||
DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; };
|
||||
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; };
|
||||
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; };
|
||||
DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553562855B02500E55709 /* LoRaConfig.swift */; };
|
||||
|
|
@ -44,11 +44,13 @@
|
|||
DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD5394FB276993AD00AD86B1 /* SwiftProtobuf */; };
|
||||
DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */; };
|
||||
DD539502276DAA6A00AD86B1 /* MapLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD539501276DAA6A00AD86B1 /* MapLocation.swift */; };
|
||||
DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */; };
|
||||
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; };
|
||||
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; };
|
||||
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; };
|
||||
DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; };
|
||||
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; };
|
||||
DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD798B062915928D005217CD /* ChannelMessageList.swift */; };
|
||||
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */; };
|
||||
DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */; };
|
||||
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169FE272476C700F4AB02 /* LogDocument.swift */; };
|
||||
|
|
@ -129,7 +131,7 @@
|
|||
DD17E5DC277D49D400010EC2 /* storeforward.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = storeforward.pb.swift; sourceTree = "<group>"; };
|
||||
DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfigEnums.swift; sourceTree = "<group>"; };
|
||||
DD1925B828CDA93900720036 /* SerialConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfigEnums.swift; sourceTree = "<group>"; };
|
||||
DD1BF2F82776FE2E008C8D2F /* MessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageList.swift; sourceTree = "<group>"; };
|
||||
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = "<group>"; };
|
||||
DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = "<group>"; };
|
||||
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
|
||||
DD2553562855B02500E55709 /* LoRaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaConfig.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -155,11 +157,13 @@
|
|||
DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentMetricsLog.swift; sourceTree = "<group>"; };
|
||||
DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DD539501276DAA6A00AD86B1 /* MapLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLocation.swift; sourceTree = "<group>"; };
|
||||
DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfig.swift; sourceTree = "<group>"; };
|
||||
DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = "<group>"; };
|
||||
DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = "<group>"; };
|
||||
DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = "<group>"; };
|
||||
DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = "<group>"; };
|
||||
DD798B062915928D005217CD /* ChannelMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMessageList.swift; sourceTree = "<group>"; };
|
||||
DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLogger.swift; sourceTree = "<group>"; };
|
||||
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLog.swift; sourceTree = "<group>"; };
|
||||
DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -491,7 +495,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DD882F5C2772E4640005BF05 /* Contacts.swift */,
|
||||
DD1BF2F82776FE2E008C8D2F /* MessageList.swift */,
|
||||
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */,
|
||||
DD798B062915928D005217CD /* ChannelMessageList.swift */,
|
||||
);
|
||||
path = Messages;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -532,6 +537,7 @@
|
|||
DDC4D567275499A500A4208E /* Persistence.swift */,
|
||||
DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */,
|
||||
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */,
|
||||
DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */,
|
||||
);
|
||||
path = Persistence;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -700,6 +706,7 @@
|
|||
DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */,
|
||||
DDAF8C6E26ED19040058C060 /* Extensions.swift in Sources */,
|
||||
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
|
||||
DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */,
|
||||
DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */,
|
||||
DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */,
|
||||
DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */,
|
||||
|
|
@ -724,7 +731,7 @@
|
|||
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */,
|
||||
DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */,
|
||||
DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */,
|
||||
DD1BF2F92776FE2E008C8D2F /* MessageList.swift in Sources */,
|
||||
DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */,
|
||||
DD3CC6C228EB9D4900FA9159 /* UpdateCoreData.swift in Sources */,
|
||||
DDB6ABE628B1406100384BA1 /* LoraConfigEnums.swift in Sources */,
|
||||
DDB2CC6E27F3EB47009C5FCC /* telemetry.pb.swift in Sources */,
|
||||
|
|
@ -773,6 +780,7 @@
|
|||
DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */,
|
||||
DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */,
|
||||
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */,
|
||||
DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */,
|
||||
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */,
|
||||
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
|
||||
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -9,3 +9,56 @@ enum BubblePosition {
|
|||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
enum Tapbacks: Int, CaseIterable, Identifiable {
|
||||
|
||||
case heart = 0
|
||||
case thumbsUp = 1
|
||||
case thumbsDown = 2
|
||||
case haHa = 3
|
||||
case exclamation = 4
|
||||
case question = 5
|
||||
case poop = 6
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var emojiString: String {
|
||||
get {
|
||||
switch self {
|
||||
case .heart:
|
||||
return "❤️"
|
||||
case .thumbsUp:
|
||||
return "👍"
|
||||
case .thumbsDown:
|
||||
return "👎"
|
||||
case .haHa:
|
||||
return "🤣"
|
||||
case .exclamation:
|
||||
return "‼️"
|
||||
case .question:
|
||||
return "❓"
|
||||
case .poop:
|
||||
return "💩"
|
||||
}
|
||||
}
|
||||
}
|
||||
var description: String {
|
||||
get {
|
||||
switch self {
|
||||
case .heart:
|
||||
return "Heart"
|
||||
case .thumbsUp:
|
||||
return "Thumbs Up"
|
||||
case .thumbsDown:
|
||||
return "Thumbs Down"
|
||||
case .haHa:
|
||||
return "HaHa"
|
||||
case .exclamation:
|
||||
return "Exclamation Mark"
|
||||
case .question:
|
||||
return "Question Mark"
|
||||
case .poop:
|
||||
return "Poop"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -593,25 +593,20 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
print("MAX PORT NUM OF 511")
|
||||
}
|
||||
|
||||
// MARK: Check for an All / Broadcast User
|
||||
// MARK: Check for an All / Broadcast User and delete it as a transition to multi channel
|
||||
let fetchBCUserRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "UserEntity")
|
||||
fetchBCUserRequest.predicate = NSPredicate(format: "num == %lld", Int64(broadcastNodeNum))
|
||||
|
||||
|
||||
do {
|
||||
let fetchedUser = try context?.fetch(fetchBCUserRequest) as! [UserEntity]
|
||||
if fetchedUser.isEmpty {
|
||||
// Save the broadcast user if it does not exist
|
||||
let bcu: UserEntity = UserEntity(context: context!)
|
||||
bcu.shortName = "ALL"
|
||||
bcu.longName = "All - Broadcast"
|
||||
bcu.hwModel = "UNSET"
|
||||
bcu.num = Int64(broadcastNodeNum)
|
||||
bcu.userId = "BROADCASTNODE"
|
||||
print("💾 Saved the All - Broadcast User")
|
||||
if fetchedUser.count > 0 {
|
||||
|
||||
context?.delete(fetchedUser[0])
|
||||
print("🗑️ Deleted the All - Broadcast User")
|
||||
}
|
||||
|
||||
|
||||
} catch {
|
||||
MeshLogger.log("💥 Error Saving the All - Broadcast User")
|
||||
MeshLogger.log("💥 Error Deleting the All - Broadcast User")
|
||||
}
|
||||
|
||||
// MARK: Share Location Position Update Timer
|
||||
|
|
@ -648,7 +643,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
}
|
||||
}
|
||||
|
||||
public func sendMessage(message: String, toUserNum: Int64, isEmoji: Bool, replyID: Int64) -> Bool {
|
||||
public func sendMessage(message: String, toUserNum: Int64, channel: Int32, isEmoji: Bool, replyID: Int64) -> Bool {
|
||||
|
||||
var success = false
|
||||
|
||||
|
|
@ -694,26 +689,16 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
newMessage.messageId = Int64(UInt32.random(in: UInt32(UInt8.max)..<UInt32.max))
|
||||
newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970)
|
||||
newMessage.receivedACK = false
|
||||
newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum })
|
||||
if toUserNum > 0 {
|
||||
newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum })
|
||||
}
|
||||
newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum })
|
||||
newMessage.isEmoji = isEmoji
|
||||
newMessage.admin = false
|
||||
|
||||
newMessage.channel = channel
|
||||
if replyID > 0 {
|
||||
|
||||
newMessage.replyID = replyID
|
||||
}
|
||||
if newMessage.toUser == nil {
|
||||
|
||||
let bcu: UserEntity = UserEntity(context: context!)
|
||||
bcu.shortName = "ALL"
|
||||
bcu.longName = "All - Broadcast"
|
||||
bcu.hwModel = "UNSET"
|
||||
bcu.num = Int64(broadcastNodeNum)
|
||||
bcu.userId = "BROADCASTNODE"
|
||||
newMessage.toUser = bcu
|
||||
}
|
||||
|
||||
newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum })
|
||||
newMessage.messagePayload = message
|
||||
|
||||
let dataType = PortNum.textMessageApp
|
||||
|
|
@ -725,7 +710,12 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
|
||||
var meshPacket = MeshPacket()
|
||||
meshPacket.id = UInt32(newMessage.messageId)
|
||||
meshPacket.to = UInt32(toUserNum)
|
||||
if toUserNum > 0 {
|
||||
meshPacket.to = UInt32(toUserNum)
|
||||
} else {
|
||||
meshPacket.to = 4294967295
|
||||
}
|
||||
meshPacket.channel = UInt32(channel)
|
||||
meshPacket.from = UInt32(fromUserNum)
|
||||
meshPacket.decoded = dataMessage
|
||||
meshPacket.decoded.emoji = isEmoji ? 1 : 0
|
||||
|
|
|
|||
|
|
@ -68,4 +68,12 @@ extension String {
|
|||
UIGraphicsEndImageContext()
|
||||
return image
|
||||
}
|
||||
|
||||
func camelCaseToWords() -> String {
|
||||
return unicodeScalars.dropFirst().reduce(String(prefix(1))) {
|
||||
return CharacterSet.uppercaseLetters.contains($1)
|
||||
? $0 + " " + String($1)
|
||||
: $0 + String($1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -771,12 +771,12 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
|
|||
newChannel.role = Int32(channel.role.rawValue)
|
||||
newChannel.psk = channel.settings.psk
|
||||
let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as! NSMutableOrderedSet
|
||||
if newChannel.index == 0 {
|
||||
mutableChannels.removeAllObjects()
|
||||
if mutableChannels.contains(newChannel) {
|
||||
mutableChannels.replaceObject(at: Int(newChannel.index), with: newChannel)
|
||||
} else {
|
||||
mutableChannels.add(newChannel)
|
||||
}
|
||||
mutableChannels.add(newChannel)
|
||||
fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet
|
||||
//fetchedMyInfo[0].objectWillChange.send()
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
|
|
@ -1260,13 +1260,6 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
|
|||
do {
|
||||
|
||||
let fetchedUsers = try context.fetch(messageUsers) as! [UserEntity]
|
||||
|
||||
if fetchedUsers.count <= 1 && fetchedUsers.first(where: { $0.num == packet.from }) == nil {
|
||||
|
||||
print("Message from another mesh, unable to manage for now")
|
||||
return
|
||||
}
|
||||
|
||||
let newMessage = MessageEntity(context: context)
|
||||
newMessage.messageId = Int64(packet.id)
|
||||
newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime)
|
||||
|
|
@ -1277,52 +1270,46 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
|
|||
if packet.decoded.replyID > 0 {
|
||||
newMessage.replyID = Int64(packet.decoded.replyID)
|
||||
}
|
||||
if packet.to == broadcastNodeNum && fetchedUsers.count == 1 {
|
||||
// Save the broadcast user if it does not exist
|
||||
let bcu: UserEntity = UserEntity(context: context)
|
||||
bcu.shortName = "ALL"
|
||||
bcu.longName = "All - Broadcast"
|
||||
bcu.hwModel = "UNSET"
|
||||
bcu.num = Int64(broadcastNodeNum)
|
||||
bcu.userId = "BROADCASTNODE"
|
||||
newMessage.toUser = bcu
|
||||
|
||||
} else {
|
||||
if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != 4294967295 {
|
||||
newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to })
|
||||
}
|
||||
newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from })
|
||||
if fetchedUsers.first(where: { $0.num == packet.from }) != nil {
|
||||
newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from })
|
||||
}
|
||||
|
||||
newMessage.messagePayload = messageText
|
||||
newMessage.fromUser?.objectWillChange.send()
|
||||
newMessage.toUser?.objectWillChange.send()
|
||||
|
||||
var messageSaved = false
|
||||
var messageSaved = false
|
||||
|
||||
do {
|
||||
do {
|
||||
|
||||
try context.save()
|
||||
MeshLogger.log("💾 Saved a new message for \(newMessage.messageId)")
|
||||
messageSaved = true
|
||||
|
||||
if messageSaved {
|
||||
if newMessage.fromUser != nil {
|
||||
// Create an iOS Notification for the received message and schedule it immediately
|
||||
let manager = LocalNotificationManager()
|
||||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? "Unknown")",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")",
|
||||
content: messageText)
|
||||
]
|
||||
manager.schedule()
|
||||
MeshLogger.log("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown")")
|
||||
}
|
||||
try context.save()
|
||||
MeshLogger.log("💾 Saved a new message for \(newMessage.messageId)")
|
||||
messageSaved = true
|
||||
|
||||
if messageSaved {
|
||||
if newMessage.fromUser != nil {
|
||||
// Create an iOS Notification for the received message and schedule it immediately
|
||||
let manager = LocalNotificationManager()
|
||||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? "Unknown")",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")",
|
||||
content: messageText)
|
||||
]
|
||||
manager.schedule()
|
||||
MeshLogger.log("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown")")
|
||||
}
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
MeshLogger.log("💥 Failed to save new MessageEntity \(nsError)")
|
||||
}
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
MeshLogger.log("💥 Failed to save new MessageEntity \(nsError)")
|
||||
}
|
||||
} catch {
|
||||
MeshLogger.log("💥 Fetch Message To and From Users Error")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="22A380" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21512" systemVersion="22A380" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
|
||||
|
|
@ -28,6 +28,9 @@
|
|||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uplinkEnabled" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
|
||||
<fetchedProperty name="allPrivateMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="channel == $FETCH_SOURCE.index && toUser == nil"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="index"/>
|
||||
|
|
@ -84,8 +87,8 @@
|
|||
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="fromUser" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="toUser" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
|
||||
<fetchedProperty name="tapbacks" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isEmoji == true"/>
|
||||
</fetchedProperty>
|
||||
|
|
@ -236,7 +239,7 @@
|
|||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(toUser.num == $FETCH_SOURCE.num) AND isEmoji == false AND admin = true"/>
|
||||
</fetchedProperty>
|
||||
<fetchedProperty name="allMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND isEmoji == false AND admin = false"/>
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false"/>
|
||||
</fetchedProperty>
|
||||
</entity>
|
||||
</model>
|
||||
15
Meshtastic/Persistence/ChannelEntityExtension.swift
Normal file
15
Meshtastic/Persistence/ChannelEntityExtension.swift
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// ChannelEntityExtension.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 11/7/22.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
extension ChannelEntity {
|
||||
|
||||
var allPrivateMessages: [MessageEntity] {
|
||||
|
||||
self.value(forKey: "allPrivateMessages") as! [MessageEntity]
|
||||
}
|
||||
}
|
||||
325
Meshtastic/Views/Messages/ChannelMessageList.swift
Normal file
325
Meshtastic/Views/Messages/ChannelMessageList.swift
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
//
|
||||
// UserMessageList.swift
|
||||
// MeshtasticApple
|
||||
//
|
||||
// Created by Garth Vander Houwen on 12/24/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct ChannelMessageList: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@EnvironmentObject var userSettings: UserSettings
|
||||
|
||||
enum Field: Hashable {
|
||||
case messageText
|
||||
}
|
||||
|
||||
// Keyboard State
|
||||
@State var typingMessage: String = ""
|
||||
@State private var totalBytes = 0
|
||||
var maxbytes = 228
|
||||
@FocusState var focusedField: Field?
|
||||
|
||||
@ObservedObject var channel: ChannelEntity
|
||||
@State var showDeleteMessageAlert = false
|
||||
@State private var deleteMessageId: Int64 = 0
|
||||
@State private var replyMessageId: Int64 = 0
|
||||
@State private var sendPositionWithMessage: Bool = false
|
||||
@State private var refreshId = UUID()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollViewReader { scrollView in
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach( channel.allPrivateMessages ) { (message: MessageEntity) in
|
||||
let currentUser: Bool = (userSettings.preferredNodeNum == message.fromUser?.num ? true : false)
|
||||
if message.replyID > 0 {
|
||||
let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID })
|
||||
HStack {
|
||||
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2)
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.blue, lineWidth: 0.5)
|
||||
)
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.blue)
|
||||
.padding(.trailing)
|
||||
}
|
||||
}
|
||||
HStack (alignment: .top) {
|
||||
if currentUser { Spacer(minLength:50) }
|
||||
if !currentUser {
|
||||
CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.darkGray), circleSize: 44, fontSize: 14)
|
||||
.padding(.all, 5)
|
||||
.offset(y: -5)
|
||||
}
|
||||
VStack(alignment: currentUser ? .trailing : .leading) {
|
||||
Text(message.messagePayload ?? "EMPTY MESSAGE")
|
||||
.padding(10)
|
||||
.foregroundColor(.white)
|
||||
.background(currentUser ? Color.blue : Color(.darkGray))
|
||||
.cornerRadius(15)
|
||||
.contextMenu {
|
||||
VStack{
|
||||
Text("Channel: \(message.channel)")
|
||||
}
|
||||
Menu("Tapback response") {
|
||||
ForEach(Tapbacks.allCases) { tb in
|
||||
Button(action: {
|
||||
if bleManager.sendMessage(message: tb.emojiString, toUserNum: 0, channel: channel.index, isEmoji: true, replyID: message.messageId) {
|
||||
print("Sent \(tb.emojiString) Tapback")
|
||||
self.context.refresh(channel, mergeChanges: true)
|
||||
} else { print("\(tb.emojiString) Tapback Failed") }
|
||||
|
||||
}) {
|
||||
Text(tb.description)
|
||||
let image = tb.emojiString.image()
|
||||
Image(uiImage: image!)
|
||||
}
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
self.replyMessageId = message.messageId
|
||||
self.focusedField = .messageText
|
||||
print("I want to reply to \(message.messageId)")
|
||||
}) {
|
||||
Text("Reply")
|
||||
Image(systemName: "arrowshape.turn.up.left.2.fill")
|
||||
}
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = message.messagePayload
|
||||
}) {
|
||||
Text("Copy")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
Menu("Message Details") {
|
||||
VStack {
|
||||
let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))
|
||||
Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray)
|
||||
}
|
||||
if currentUser && message.receivedACK {
|
||||
VStack {
|
||||
Text("Received Ack \(message.receivedACK ? "✔️" : "")")
|
||||
}
|
||||
} else if currentUser && message.ackError == 0 {
|
||||
// Empty Error
|
||||
Text("Waiting. . .")
|
||||
} else if currentUser && message.ackError > 0 {
|
||||
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
|
||||
Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
if currentUser {
|
||||
VStack {
|
||||
let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp))
|
||||
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
|
||||
if ackDate >= sixMonthsAgo! {
|
||||
Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray)
|
||||
} else {
|
||||
Text("Unknown Age").font(.caption2).foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
if message.ackSNR != 0 {
|
||||
VStack {
|
||||
Text("Ack SNR \(String(message.ackSNR))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
Button(role: .destructive, action: {
|
||||
self.showDeleteMessageAlert = true
|
||||
self.deleteMessageId = message.messageId
|
||||
print(deleteMessageId)
|
||||
}) {
|
||||
Text("Delete")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity]
|
||||
if tapbacks.count > 0 {
|
||||
VStack (alignment: .trailing) {
|
||||
HStack {
|
||||
ForEach( tapbacks ) { (tapback: MessageEntity) in
|
||||
VStack {
|
||||
let image = tapback.messagePayload!.image(fontSize: 20)
|
||||
Image(uiImage: image!).font(.caption)
|
||||
Text("\(tapback.fromUser?.shortName ?? "????")")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
.padding(.bottom, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.gray, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
if currentUser && message.receivedACK {
|
||||
// Ack Received
|
||||
Text("Acknowledged").font(.caption2).foregroundColor(.gray)
|
||||
} else if currentUser && message.ackError == 0 {
|
||||
// Empty Error
|
||||
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange)
|
||||
} else if currentUser && message.ackError > 0 {
|
||||
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
|
||||
Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true)
|
||||
.font(.caption2).foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
.id(channel.allPrivateMessages.firstIndex(of: message))
|
||||
if !currentUser {
|
||||
Spacer(minLength:50)
|
||||
}
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.frame(maxWidth: .infinity)
|
||||
.id(message.messageId)
|
||||
.alert(isPresented: $showDeleteMessageAlert) {
|
||||
Alert(title: Text("Are you sure you want to delete this message?"), message: Text("This action is permanent."), primaryButton: .destructive(Text("Delete")) {
|
||||
print("OK button tapped")
|
||||
if deleteMessageId > 0 {
|
||||
let message = channel.allPrivateMessages.first(where: { $0.messageId == deleteMessageId })
|
||||
context.delete(message!)
|
||||
do {
|
||||
try context.save()
|
||||
deleteMessageId = 0
|
||||
} catch {
|
||||
print("Failed to delete message \(deleteMessageId)")
|
||||
}
|
||||
}
|
||||
}, secondaryButton: .cancel())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.top])
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.onAppear(perform: {
|
||||
self.bleManager.context = context
|
||||
refreshId = UUID()
|
||||
if channel.allPrivateMessages.count > 0 {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last!.messageId)
|
||||
}
|
||||
})
|
||||
.onChange(of: channel.allPrivateMessages, perform: { messages in
|
||||
refreshId = UUID()
|
||||
if channel.allPrivateMessages.count > 0 {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last!.messageId)
|
||||
}
|
||||
})
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
|
||||
ZStack {
|
||||
let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0)
|
||||
TextField("Message", text: $typingMessage, axis: .vertical)
|
||||
.onChange(of: typingMessage, perform: { value in
|
||||
totalBytes = value.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > maxbytes {
|
||||
let firstNBytes = Data(typingMessage.utf8.prefix(maxbytes))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the message back to the last place where it was the right size
|
||||
typingMessage = maxBytesString
|
||||
} else {
|
||||
print("not a valid UTF-8 sequence")
|
||||
}
|
||||
}
|
||||
})
|
||||
.keyboardType(kbType!)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Button("Dismiss Keyboard") {
|
||||
focusedField = nil
|
||||
}
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
Button {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
if userSettings.meshtasticUsername.count > 0 {
|
||||
|
||||
typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName
|
||||
|
||||
} else {
|
||||
|
||||
typingMessage = "📍 " + userLongName + " has shared their position with you."
|
||||
}
|
||||
|
||||
} label: {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
|
||||
.frame(width: 130)
|
||||
.padding(5)
|
||||
.font(.subheadline)
|
||||
.accentColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.focused($focusedField, equals: .messageText)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(minHeight: 50)
|
||||
|
||||
Text(typingMessage).opacity(0).padding(.all, 0)
|
||||
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
|
||||
.padding(.bottom, 15)
|
||||
Button(action: {
|
||||
if bleManager.sendMessage(message: typingMessage, toUserNum: 0, channel: channel.index, isEmoji: false, replyID: replyMessageId) {
|
||||
typingMessage = ""
|
||||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendLocation(destNum: Int64(channel.index), wantAck: true) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
CircleText(text: String(channel.index), color: .blue, circleSize: 44, fontSize: 30).fixedSize()
|
||||
Text(String(channel.name ?? "Unknown").camelCaseToWords()).font(.headline)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct Contacts: View {
|
||||
|
||||
|
|
@ -18,17 +19,7 @@ struct Contacts: View {
|
|||
animation: .default)
|
||||
|
||||
private var users: FetchedResults<UserEntity>
|
||||
|
||||
|
||||
|
||||
private var prefferedNode: NodeInfoEntity?
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(key: "num", ascending: true)],
|
||||
animation: .default)
|
||||
|
||||
private var nodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
@State var node: NodeInfoEntity? = nil
|
||||
|
||||
@State private var selection: UserEntity? = nil // Nothing selected by default.
|
||||
|
||||
|
|
@ -36,96 +27,125 @@ struct Contacts: View {
|
|||
|
||||
NavigationSplitView {
|
||||
List {
|
||||
Section(header: Text("Primary Channel")) {
|
||||
ForEach(users) { (user: UserEntity) in
|
||||
|
||||
if user.num != bleManager.userSettings?.preferredNodeNum ?? 0 {
|
||||
Section(header: Text("Channels (groups)")) {
|
||||
// Display Contacts for the rest of the non admin channels
|
||||
if node != nil {
|
||||
ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in
|
||||
if channel.name?.lowercased() ?? "" != "admin" && channel.name?.lowercased() ?? "" != "gpio" {
|
||||
VStack {
|
||||
NavigationLink(destination: ChannelMessageList(channel: channel)) {
|
||||
|
||||
NavigationLink(destination: MessageList(user: user)) {
|
||||
|
||||
if user.messageList.count > 0 {
|
||||
|
||||
let mostRecent = user.num == bleManager.broadcastNodeNum ? user.messageList.last : user.messageList.last(where: { $0.toUser?.num ?? 0 != bleManager.broadcastNodeNum })
|
||||
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
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
CircleText(text: user.shortName ?? "???", color: Color.blue)
|
||||
.padding(.trailing, 5)
|
||||
}
|
||||
VStack {
|
||||
HStack {
|
||||
VStack {
|
||||
Text(user.longName ?? "Unknown").font(.headline).fixedSize()
|
||||
}
|
||||
VStack {
|
||||
let mostRecent = channel.allPrivateMessages.last
|
||||
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
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
CircleText(text: String(channel.index), color: Color.blue, circleSize: 52, fontSize: 40)
|
||||
.padding(.trailing, 5)
|
||||
VStack {
|
||||
Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
if channel.allPrivateMessages.count > 0 {
|
||||
VStack (alignment: .trailing) {
|
||||
if lastMessageDay == currentDay {
|
||||
Text(lastMessageTime, style: .time )
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
} else if lastMessageDay == (currentDay - 1) {
|
||||
Text("Yesterday")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
} else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) {
|
||||
Text(lastMessageTime.formattedDate(format: "MM/dd/yy"))
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
} else if lastMessageDay < (currentDay - 1800) {
|
||||
Text(lastMessageTime.formattedDate(format: "MM/dd/yy"))
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if channel.allPrivateMessages.count > 0 {
|
||||
HStack(alignment: .top) {
|
||||
Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")")
|
||||
.truncationMode(.tail)
|
||||
.foregroundColor(Color.gray)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
}
|
||||
Section(header: Text("Direct Messages (Primary Channel)")) {
|
||||
ForEach(users) { (user: UserEntity) in
|
||||
if user.num != bleManager.userSettings?.preferredNodeNum ?? 0 {
|
||||
NavigationLink(destination: UserMessageList(user: user)) {
|
||||
let mostRecent = user.num == bleManager.broadcastNodeNum ? user.messageList.last : user.messageList.last(where: { $0.toUser?.num ?? 0 != bleManager.broadcastNodeNum })
|
||||
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
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
CircleText(text: user.shortName ?? "???", color: Color.blue, circleSize: 52, fontSize: 16)
|
||||
.padding(.trailing, 5)
|
||||
VStack {
|
||||
Text(user.longName ?? "Unknown").font(.headline)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
if user.messageList.count > 0 {
|
||||
VStack (alignment: .trailing) {
|
||||
if lastMessageDay == currentDay {
|
||||
Text(lastMessageTime, style: .time )
|
||||
.font(.caption)
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
} else if lastMessageDay == (currentDay - 1) {
|
||||
|
||||
Text("Yesterday")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
} else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) {
|
||||
Text(lastMessageTime, style: .date)
|
||||
Text(lastMessageTime.formattedDate(format: "MM/dd/yy"))
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
} else if lastMessageDay < (currentDay - 1800) {
|
||||
Text(lastMessageTime, style: .date)
|
||||
Text(lastMessageTime.formattedDate(format: "MM/dd/yy"))
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
if user.messageList.count > 0 {
|
||||
HStack(alignment: .top) {
|
||||
Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")")
|
||||
.frame(height: 50)
|
||||
.truncationMode(.tail)
|
||||
.foregroundColor(Color.gray)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
CircleText(text: user.shortName ?? "???", color: Color.blue)
|
||||
.padding(.trailing, 5)
|
||||
}
|
||||
VStack {
|
||||
HStack {
|
||||
VStack {
|
||||
Text(user.longName ?? "Unknown").font(.headline).fixedSize()
|
||||
}
|
||||
VStack {
|
||||
Text(" ")
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
Text(" ")
|
||||
.frame(height: 50 )
|
||||
.truncationMode(.tail)
|
||||
.foregroundColor(Color.gray)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(header: Text("Private Channels")) {
|
||||
// Display Contacts for the rest of the non admin channels
|
||||
|
||||
}
|
||||
.hidden()
|
||||
}
|
||||
.tint(Color(UIColor.systemGray))
|
||||
.navigationSplitViewStyle(.automatic)
|
||||
|
|
@ -134,18 +154,37 @@ struct Contacts: View {
|
|||
.navigationBarItems(leading:
|
||||
MeshtasticLogo()
|
||||
)
|
||||
.onAppear {
|
||||
self.bleManager.userSettings = userSettings
|
||||
self.bleManager.context = context
|
||||
|
||||
if userSettings.preferredNodeNum > 0 {
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(userSettings.preferredNodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, check it for a region
|
||||
if !fetchedNode.isEmpty {
|
||||
node = fetchedNode[0]
|
||||
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
detail: {
|
||||
|
||||
if let user = selection {
|
||||
|
||||
MessageList(user:user)
|
||||
UserMessageList(user:user)
|
||||
|
||||
} else {
|
||||
|
||||
Text("Select a user")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,439 +0,0 @@
|
|||
//
|
||||
// UserMessageList.swift
|
||||
// MeshtasticApple
|
||||
//
|
||||
// Created by Garth Vander Houwen on 12/24/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct MessageList: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@EnvironmentObject var userSettings: UserSettings
|
||||
|
||||
enum Field: Hashable {
|
||||
case messageText
|
||||
}
|
||||
|
||||
// Keyboard State
|
||||
@State var typingMessage: String = ""
|
||||
@State private var totalBytes = 0
|
||||
var maxbytes = 228
|
||||
@FocusState var focusedField: Field?
|
||||
|
||||
@ObservedObject var user: UserEntity
|
||||
@State var showDeleteMessageAlert = false
|
||||
@State private var deleteMessageId: Int64 = 0
|
||||
@State private var replyMessageId: Int64 = 0
|
||||
@State private var sendPositionWithMessage: Bool = false
|
||||
@State private var refreshId = UUID()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollViewReader { scrollView in
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach( user.messageList ) { (message: MessageEntity) in
|
||||
if user.num != userSettings.preferredNodeNum {
|
||||
let currentUser: Bool = (userSettings.preferredNodeNum == message.fromUser?.num ? true : false)
|
||||
if (user.num == bleManager.broadcastNodeNum || user.num != bleManager.broadcastNodeNum && message.toUser!.num != bleManager.broadcastNodeNum) {
|
||||
if message.replyID > 0 {
|
||||
let messageReply = user.messageList.first(where: { $0.messageId == message.replyID })
|
||||
HStack {
|
||||
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2)
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.blue, lineWidth: 0.5)
|
||||
)
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.blue)
|
||||
.padding(.trailing)
|
||||
}
|
||||
}
|
||||
HStack (alignment: .top) {
|
||||
if currentUser { Spacer(minLength:50) }
|
||||
if !currentUser {
|
||||
CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.darkGray), circleSize: 44, fontSize: 14)
|
||||
.padding(.all, 5)
|
||||
.offset(y: -5)
|
||||
}
|
||||
VStack(alignment: currentUser ? .trailing : .leading) {
|
||||
Text(message.messagePayload ?? "EMPTY MESSAGE")
|
||||
.padding(10)
|
||||
.foregroundColor(.white)
|
||||
.background(currentUser ? Color.blue : Color(.darkGray))
|
||||
.cornerRadius(15)
|
||||
.contextMenu {
|
||||
Menu("Tapback response") {
|
||||
|
||||
Button(action: {
|
||||
if bleManager.sendMessage(message: "❤️", toUserNum: user.num, isEmoji: true, replyID: message.messageId) {
|
||||
print("Sent ❤️ Tapback")
|
||||
self.context.refresh(user, mergeChanges: true)
|
||||
} else { print("❤️ Tapback Failed") }
|
||||
|
||||
}) {
|
||||
Text("Heart")
|
||||
let image = "❤️".image()
|
||||
Image(uiImage: image!)
|
||||
}
|
||||
Button(action: {
|
||||
|
||||
if bleManager.sendMessage(message: "👍", toUserNum: user.num, isEmoji: true, replyID: message.messageId) {
|
||||
|
||||
print("Sent 👍 Tapback")
|
||||
self.context.refresh(user, mergeChanges: true)
|
||||
|
||||
} else { print("👍 Tapback Failed")}
|
||||
|
||||
}) {
|
||||
Text("Thumbs Up")
|
||||
let image = "👍".image()
|
||||
Image(uiImage: image!)
|
||||
}
|
||||
Button(action: {
|
||||
|
||||
if bleManager.sendMessage(message: "👎", toUserNum: user.num, isEmoji: true, replyID: message.messageId) {
|
||||
|
||||
print("Sent 👎 Tapback")
|
||||
self.context.refresh(user, mergeChanges: true)
|
||||
|
||||
} else { print("👎 Tapback Failed") }
|
||||
|
||||
}) {
|
||||
Text("Thumbs Down")
|
||||
let image = "👎".image()
|
||||
Image(uiImage: image!)
|
||||
}
|
||||
Button(action: {
|
||||
|
||||
if bleManager.sendMessage(message: "🤣", toUserNum: user.num, isEmoji: true, replyID: message.messageId) {
|
||||
|
||||
print("Sent 🤣 Tapback")
|
||||
self.context.refresh(user, mergeChanges: true)
|
||||
|
||||
} else { print("🤣 Tapback Failed") }
|
||||
|
||||
}) {
|
||||
Text("HaHa")
|
||||
let image = "🤣".image()
|
||||
Image(uiImage: image!)
|
||||
}
|
||||
Button(action: {
|
||||
|
||||
if bleManager.sendMessage(message: "‼️", toUserNum: user.num, isEmoji: true, replyID: message.messageId) {
|
||||
|
||||
print("Sent ‼️ Tapback")
|
||||
self.context.refresh(user, mergeChanges: true)
|
||||
|
||||
} else { print("‼️ Tapback Failed") }
|
||||
|
||||
}) {
|
||||
Text("Exclamation Mark")
|
||||
let image = "‼️".image()
|
||||
Image(uiImage: image!)
|
||||
}
|
||||
Button(action: {
|
||||
if bleManager.sendMessage(message: "❓", toUserNum: user.num, isEmoji: true, replyID: message.messageId) {
|
||||
self.context.refresh(user, mergeChanges: true)
|
||||
print("Sent ❓ Tapback")
|
||||
} else { print("❓ Tapback Failed") }
|
||||
}) {
|
||||
Text("Question Mark")
|
||||
let image = "❓".image()
|
||||
Image(uiImage: image!)
|
||||
}
|
||||
Button(action: {
|
||||
if bleManager.sendMessage(message: "💩", toUserNum: user.num, isEmoji: true, replyID: message.messageId) {
|
||||
self.context.refresh(user, mergeChanges: true)
|
||||
print("Sent 💩 Tapback")
|
||||
} else { print("💩 Tapback Failed") }
|
||||
}) {
|
||||
Text("Poop")
|
||||
let image = "💩".image()
|
||||
Image(uiImage: image!)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
self.replyMessageId = message.messageId
|
||||
self.focusedField = .messageText
|
||||
|
||||
print("I want to reply to \(message.messageId)")
|
||||
}) {
|
||||
Text("Reply")
|
||||
Image(systemName: "arrowshape.turn.up.left.2.fill")
|
||||
}
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = message.messagePayload
|
||||
}) {
|
||||
Text("Copy")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
Menu("Message Details") {
|
||||
VStack {
|
||||
let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))
|
||||
Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray)
|
||||
}
|
||||
if currentUser && message.receivedACK {
|
||||
|
||||
VStack {
|
||||
|
||||
Text("Received Ack \(message.receivedACK ? "✔️" : "")")
|
||||
}
|
||||
|
||||
} else if currentUser && message.ackError == 0 {
|
||||
|
||||
// Empty Error
|
||||
Text("Waiting. . .")
|
||||
|
||||
} else if currentUser && message.ackError > 0 {
|
||||
|
||||
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
|
||||
Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
if currentUser {
|
||||
|
||||
VStack {
|
||||
|
||||
let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp))
|
||||
|
||||
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
|
||||
if ackDate >= sixMonthsAgo! {
|
||||
|
||||
Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray)
|
||||
|
||||
} else {
|
||||
|
||||
Text("Unknown Age").font(.caption2).foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if message.ackSNR != 0 {
|
||||
VStack {
|
||||
|
||||
Text("Ack SNR \(String(message.ackSNR))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
Button(role: .destructive, action: {
|
||||
self.showDeleteMessageAlert = true
|
||||
self.deleteMessageId = message.messageId
|
||||
print(deleteMessageId)
|
||||
}) {
|
||||
Text("Delete")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity]
|
||||
if tapbacks.count > 0 {
|
||||
|
||||
VStack (alignment: .trailing) {
|
||||
|
||||
HStack {
|
||||
|
||||
ForEach( tapbacks ) { (tapback: MessageEntity) in
|
||||
|
||||
VStack {
|
||||
|
||||
let image = tapback.messagePayload!.image(fontSize: 20)
|
||||
Image(uiImage: image!).font(.caption)
|
||||
Text("\(tapback.fromUser?.shortName ?? "????")")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
.padding(.bottom, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.gray, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
if currentUser && message.receivedACK {
|
||||
// Ack Received
|
||||
Text("Acknowledged").font(.caption2).foregroundColor(.gray)
|
||||
} else if currentUser && message.ackError == 0 {
|
||||
// Empty Error
|
||||
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange)
|
||||
} else if currentUser && message.ackError > 0 {
|
||||
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
|
||||
Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true)
|
||||
.font(.caption2).foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
.id(user.messageList.firstIndex(of: message))
|
||||
if !currentUser {
|
||||
Spacer(minLength:50)
|
||||
}
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.frame(maxWidth: .infinity)
|
||||
.id(message.messageId)
|
||||
.alert(isPresented: $showDeleteMessageAlert) {
|
||||
Alert(title: Text("Are you sure you want to delete this message?"), message: Text("This action is permanent."), primaryButton: .destructive(Text("Delete")) {
|
||||
print("OK button tapped")
|
||||
if deleteMessageId > 0 {
|
||||
let message = user.messageList.first(where: { $0.messageId == deleteMessageId })
|
||||
context.delete(message!)
|
||||
do {
|
||||
try context.save()
|
||||
deleteMessageId = 0
|
||||
} catch {
|
||||
print("Failed to delete message \(deleteMessageId)")
|
||||
}
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.top])
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.onAppear(perform: {
|
||||
self.bleManager.context = context
|
||||
refreshId = UUID()
|
||||
if user.messageList.count > 0 {
|
||||
scrollView.scrollTo(user.messageList.last!.messageId)
|
||||
}
|
||||
})
|
||||
.onChange(of: user.messageList, perform: { messages in
|
||||
refreshId = UUID()
|
||||
if user.messageList.count > 0 {
|
||||
scrollView.scrollTo(user.messageList.last!.messageId)
|
||||
}
|
||||
})
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
|
||||
ZStack {
|
||||
let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0)
|
||||
TextField("Message", text: $typingMessage, axis: .vertical)
|
||||
.onChange(of: typingMessage, perform: { value in
|
||||
totalBytes = value.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > maxbytes {
|
||||
let firstNBytes = Data(typingMessage.utf8.prefix(maxbytes))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the message back to the last place where it was the right size
|
||||
typingMessage = maxBytesString
|
||||
} else {
|
||||
print("not a valid UTF-8 sequence")
|
||||
}
|
||||
}
|
||||
})
|
||||
.keyboardType(kbType!)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
|
||||
Button("Dismiss Keyboard") {
|
||||
focusedField = nil
|
||||
}
|
||||
.font(.subheadline)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
if user.num == bleManager.broadcastNodeNum {
|
||||
|
||||
if userSettings.meshtasticUsername.count > 0 {
|
||||
|
||||
typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with the mesh from node " + userLongName
|
||||
} else {
|
||||
|
||||
typingMessage = "📍 " + userLongName + " has shared their position with the mesh."
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if userSettings.meshtasticUsername.count > 0 {
|
||||
|
||||
typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName
|
||||
|
||||
} else {
|
||||
|
||||
typingMessage = "📍 " + userLongName + " has shared their position with you."
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
|
||||
.frame(width: 130)
|
||||
.padding(5)
|
||||
.font(.subheadline)
|
||||
.accentColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.focused($focusedField, equals: .messageText)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(minHeight: 50)
|
||||
|
||||
Text(typingMessage).opacity(0).padding(.all, 0)
|
||||
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
|
||||
.padding(.bottom, 15)
|
||||
Button(action: {
|
||||
if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, isEmoji: false, replyID: replyMessageId) {
|
||||
typingMessage = ""
|
||||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendLocation(destNum: user.num, wantAck: true) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
CircleText(text: user.shortName ?? "???", color: .blue, circleSize: 42, fontSize: 16).fixedSize()
|
||||
Text(user.longName ?? "Unknown").font(.headline).fixedSize()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
341
Meshtastic/Views/Messages/UserMessageList.swift
Normal file
341
Meshtastic/Views/Messages/UserMessageList.swift
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
//
|
||||
// UserMessageList.swift
|
||||
// MeshtasticApple
|
||||
//
|
||||
// Created by Garth Vander Houwen on 12/24/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct UserMessageList: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@EnvironmentObject var userSettings: UserSettings
|
||||
|
||||
enum Field: Hashable {
|
||||
case messageText
|
||||
}
|
||||
|
||||
// Keyboard State
|
||||
@State var typingMessage: String = ""
|
||||
@State private var totalBytes = 0
|
||||
var maxbytes = 228
|
||||
@FocusState var focusedField: Field?
|
||||
|
||||
@ObservedObject var user: UserEntity
|
||||
@State var showDeleteMessageAlert = false
|
||||
@State private var deleteMessageId: Int64 = 0
|
||||
@State private var replyMessageId: Int64 = 0
|
||||
@State private var sendPositionWithMessage: Bool = false
|
||||
@State private var refreshId = UUID()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollViewReader { scrollView in
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach( user.messageList ) { (message: MessageEntity) in
|
||||
if user.num != userSettings.preferredNodeNum {
|
||||
let currentUser: Bool = (userSettings.preferredNodeNum == message.fromUser?.num ? true : false)
|
||||
|
||||
if message.replyID > 0 {
|
||||
let messageReply = user.messageList.first(where: { $0.messageId == message.replyID })
|
||||
HStack {
|
||||
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2)
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.blue, lineWidth: 0.5)
|
||||
)
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.blue)
|
||||
.padding(.trailing)
|
||||
}
|
||||
}
|
||||
HStack (alignment: .top) {
|
||||
if currentUser { Spacer(minLength:50) }
|
||||
if !currentUser {
|
||||
CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.darkGray), circleSize: 44, fontSize: 14)
|
||||
.padding(.all, 5)
|
||||
.offset(y: -5)
|
||||
}
|
||||
VStack(alignment: currentUser ? .trailing : .leading) {
|
||||
Text(message.messagePayload ?? "EMPTY MESSAGE")
|
||||
.padding(10)
|
||||
.foregroundColor(.white)
|
||||
.background(currentUser ? Color.blue : Color(.darkGray))
|
||||
.cornerRadius(15)
|
||||
.contextMenu {
|
||||
VStack{
|
||||
Text("Channel: \(message.channel)")
|
||||
}
|
||||
Menu("Tapback response") {
|
||||
ForEach(Tapbacks.allCases) { tb in
|
||||
Button(action: {
|
||||
if bleManager.sendMessage(message: tb.emojiString, toUserNum: user.num, channel: 0, isEmoji: true, replyID: message.messageId) {
|
||||
print("Sent \(tb.emojiString) Tapback")
|
||||
self.context.refresh(user, mergeChanges: true)
|
||||
} else { print("\(tb.emojiString) Tapback Failed") }
|
||||
|
||||
}) {
|
||||
Text(tb.description)
|
||||
let image = tb.emojiString.image()
|
||||
Image(uiImage: image!)
|
||||
}
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
self.replyMessageId = message.messageId
|
||||
self.focusedField = .messageText
|
||||
print("I want to reply to \(message.messageId)")
|
||||
}) {
|
||||
Text("Reply")
|
||||
Image(systemName: "arrowshape.turn.up.left.2.fill")
|
||||
}
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = message.messagePayload
|
||||
}) {
|
||||
Text("Copy")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
Menu("Message Details") {
|
||||
VStack {
|
||||
let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))
|
||||
Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray)
|
||||
}
|
||||
if currentUser && message.receivedACK {
|
||||
VStack {
|
||||
Text("Received Ack \(message.receivedACK ? "✔️" : "")")
|
||||
}
|
||||
} else if currentUser && message.ackError == 0 {
|
||||
// Empty Error
|
||||
Text("Waiting. . .")
|
||||
} else if currentUser && message.ackError > 0 {
|
||||
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
|
||||
Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
if currentUser {
|
||||
VStack {
|
||||
let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp))
|
||||
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
|
||||
if ackDate >= sixMonthsAgo! {
|
||||
Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray)
|
||||
} else {
|
||||
Text("Unknown Age").font(.caption2).foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
if message.ackSNR != 0 {
|
||||
VStack {
|
||||
Text("Ack SNR \(String(message.ackSNR))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
Button(role: .destructive, action: {
|
||||
self.showDeleteMessageAlert = true
|
||||
self.deleteMessageId = message.messageId
|
||||
print(deleteMessageId)
|
||||
}) {
|
||||
Text("Delete")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity]
|
||||
if tapbacks.count > 0 {
|
||||
VStack (alignment: .trailing) {
|
||||
HStack {
|
||||
ForEach( tapbacks ) { (tapback: MessageEntity) in
|
||||
VStack {
|
||||
let image = tapback.messagePayload!.image(fontSize: 20)
|
||||
Image(uiImage: image!).font(.caption)
|
||||
Text("\(tapback.fromUser?.shortName ?? "????")")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
.padding(.bottom, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.gray, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
if currentUser && message.receivedACK {
|
||||
// Ack Received
|
||||
Text("Acknowledged").font(.caption2).foregroundColor(.gray)
|
||||
} else if currentUser && message.ackError == 0 {
|
||||
// Empty Error
|
||||
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange)
|
||||
} else if currentUser && message.ackError > 0 {
|
||||
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
|
||||
Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true)
|
||||
.font(.caption2).foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
.id(user.messageList.firstIndex(of: message))
|
||||
if !currentUser {
|
||||
Spacer(minLength:50)
|
||||
}
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.frame(maxWidth: .infinity)
|
||||
.id(message.messageId)
|
||||
.alert(isPresented: $showDeleteMessageAlert) {
|
||||
Alert(title: Text("Are you sure you want to delete this message?"), message: Text("This action is permanent."), primaryButton: .destructive(Text("Delete")) {
|
||||
print("OK button tapped")
|
||||
if deleteMessageId > 0 {
|
||||
let message = user.messageList.first(where: { $0.messageId == deleteMessageId })
|
||||
context.delete(message!)
|
||||
do {
|
||||
try context.save()
|
||||
deleteMessageId = 0
|
||||
} catch {
|
||||
print("Failed to delete message \(deleteMessageId)")
|
||||
}
|
||||
}
|
||||
}, secondaryButton: .cancel())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.top])
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.onAppear(perform: {
|
||||
self.bleManager.context = context
|
||||
refreshId = UUID()
|
||||
if user.messageList.count > 0 {
|
||||
scrollView.scrollTo(user.messageList.last!.messageId)
|
||||
}
|
||||
})
|
||||
.onChange(of: user.messageList, perform: { messages in
|
||||
refreshId = UUID()
|
||||
if user.messageList.count > 0 {
|
||||
scrollView.scrollTo(user.messageList.last!.messageId)
|
||||
}
|
||||
})
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
|
||||
ZStack {
|
||||
let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0)
|
||||
TextField("Message", text: $typingMessage, axis: .vertical)
|
||||
.onChange(of: typingMessage, perform: { value in
|
||||
totalBytes = value.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > maxbytes {
|
||||
let firstNBytes = Data(typingMessage.utf8.prefix(maxbytes))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the message back to the last place where it was the right size
|
||||
typingMessage = maxBytesString
|
||||
} else {
|
||||
print("not a valid UTF-8 sequence")
|
||||
}
|
||||
}
|
||||
})
|
||||
.keyboardType(kbType!)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Button("Dismiss Keyboard") {
|
||||
focusedField = nil
|
||||
}
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
Button {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
if user.num == bleManager.broadcastNodeNum {
|
||||
|
||||
if userSettings.meshtasticUsername.count > 0 {
|
||||
|
||||
typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with the mesh from node " + userLongName
|
||||
} else {
|
||||
|
||||
typingMessage = "📍 " + userLongName + " has shared their position with the mesh."
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if userSettings.meshtasticUsername.count > 0 {
|
||||
|
||||
typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName
|
||||
|
||||
} else {
|
||||
|
||||
typingMessage = "📍 " + userLongName + " has shared their position with you."
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
|
||||
.frame(width: 130)
|
||||
.padding(5)
|
||||
.font(.subheadline)
|
||||
.accentColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.focused($focusedField, equals: .messageText)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(minHeight: 50)
|
||||
|
||||
Text(typingMessage).opacity(0).padding(.all, 0)
|
||||
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
|
||||
.padding(.bottom, 15)
|
||||
Button(action: {
|
||||
if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, channel: 0, isEmoji: false, replyID: replyMessageId) {
|
||||
typingMessage = ""
|
||||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendLocation(destNum: user.num, wantAck: true) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
CircleText(text: user.shortName ?? "???", color: .blue, circleSize: 44, fontSize: 14).fixedSize()
|
||||
Text(user.longName ?? "Unknown").font(.headline)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -431,8 +431,7 @@ struct NodeDetail: View {
|
|||
.offset( y:-40)
|
||||
}
|
||||
.edgesIgnoringSafeArea([.leading, .trailing])
|
||||
.navigationTitle((node.user != nil) ? String(node.user!.longName ?? "Unknown") : "Unknown")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarTitle((node.user != nil) ? String(node.user!.longName ?? "Unknown") : "Unknown", displayMode: .inline)
|
||||
.padding(.bottom, 10)
|
||||
.navigationBarItems(trailing:
|
||||
|
||||
|
|
|
|||
|
|
@ -24,36 +24,30 @@ struct PositionLog: View {
|
|||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
//Add a table for mac and ipad
|
||||
VStack {
|
||||
Table(node.positions!.reversed() as! [PositionEntity]) {
|
||||
TableColumn("Seq No") { position in
|
||||
Text(String(position.seqNo))
|
||||
}
|
||||
//.width(75)
|
||||
TableColumn("Latitude") { position in
|
||||
Text(String(format: "%.6f", position.latitude ?? 0))
|
||||
}
|
||||
TableColumn("Longitude") { position in
|
||||
Text(String(format: "%.6f", position.longitude ?? 0))
|
||||
}
|
||||
TableColumn("Altitude") { position in
|
||||
Text(String(position.altitude))
|
||||
}
|
||||
//.width(75)
|
||||
TableColumn("Sats") { position in
|
||||
Text(String(position.satsInView))
|
||||
}
|
||||
//.width(75)
|
||||
TableColumn("Speed") { position in
|
||||
Text(String(position.speed))
|
||||
}
|
||||
//.width(75)
|
||||
TableColumn("Heading") { position in
|
||||
Text(String(position.heading))
|
||||
}
|
||||
TableColumn("Time Stamp") { position in
|
||||
Text(position.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
|
||||
}
|
||||
Table(node.positions!.reversed() as! [PositionEntity]) {
|
||||
TableColumn("SeqNo") { position in
|
||||
Text(String(position.seqNo))
|
||||
}
|
||||
TableColumn("Latitude") { position in
|
||||
Text(String(format: "%.6f", position.latitude ?? 0))
|
||||
}
|
||||
TableColumn("Longitude") { position in
|
||||
Text(String(format: "%.6f", position.longitude ?? 0))
|
||||
}
|
||||
TableColumn("Altitude") { position in
|
||||
Text(String(position.altitude))
|
||||
}
|
||||
TableColumn("Sats") { position in
|
||||
Text(String(position.satsInView))
|
||||
}
|
||||
TableColumn("Speed") { position in
|
||||
Text(String(position.speed))
|
||||
}
|
||||
TableColumn("Heading") { position in
|
||||
Text(String(position.heading))
|
||||
}
|
||||
TableColumn("Time Stamp") { position in
|
||||
Text(position.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,223 +49,221 @@ struct ShareChannels: View {
|
|||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
GeometryReader { bounds in
|
||||
let smallest = min(bounds.size.width, bounds.size.height)
|
||||
ScrollView {
|
||||
VStack {
|
||||
if node != nil && node?.myInfo != nil {
|
||||
Grid(alignment: .top, horizontalSpacing: 2) {
|
||||
// VStack {
|
||||
GeometryReader { bounds in
|
||||
let smallest = min(bounds.size.width, bounds.size.height)
|
||||
ScrollView {
|
||||
if node != nil && node?.myInfo != nil {
|
||||
Grid() {
|
||||
GridRow {
|
||||
Spacer()
|
||||
Text("Include")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(.trailing)
|
||||
Text("Channel")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(.trailing)
|
||||
Text("Encrypted")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in
|
||||
GridRow {
|
||||
Spacer()
|
||||
Text("Include")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(.trailing)
|
||||
Text("Channel")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(.trailing)
|
||||
Text("Encrypted")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in
|
||||
GridRow {
|
||||
Spacer()
|
||||
if channel.index == 0 {
|
||||
Toggle("Channel 0 Included", isOn: $includeChannel0)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary")
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
if channel.index == 0 {
|
||||
Toggle("Channel 0 Included", isOn: $includeChannel0)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords()).fixedSize()
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 1 && channel.role > 0 {
|
||||
Toggle("Channel 1 Included", isOn: $includeChannel1)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 2 && channel.role > 0 {
|
||||
Toggle("Channel 2 Included", isOn: $includeChannel2)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 3 && channel.role > 0 {
|
||||
Toggle("Channel 3 Included", isOn: $includeChannel3)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 4 && channel.role > 0 {
|
||||
Toggle("Channel 4 Included", isOn: $includeChannel4)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 5 && channel.role > 0 {
|
||||
Toggle("Channel 5 Included", isOn: $includeChannel5)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 6 && channel.role > 0 {
|
||||
Toggle("Channel 6 Included", isOn: $includeChannel6)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 7 && channel.role > 0 {
|
||||
Toggle("Channel 7 Included", isOn: $includeChannel7)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 1 && channel.role > 0 {
|
||||
Toggle("Channel 1 Included", isOn: $includeChannel1)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)")
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 2 && channel.role > 0 {
|
||||
Toggle("Channel 2 Included", isOn: $includeChannel2)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)")
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 3 && channel.role > 0 {
|
||||
Toggle("Channel 3 Included", isOn: $includeChannel3)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)")
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 4 && channel.role > 0 {
|
||||
Toggle("Channel 4 Included", isOn: $includeChannel4)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)")
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 5 && channel.role > 0 {
|
||||
Toggle("Channel 5 Included", isOn: $includeChannel5)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)")
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 6 && channel.role > 0 {
|
||||
Toggle("Channel 6 Included", isOn: $includeChannel6)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)")
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if channel.index == 7 && channel.role > 0 {
|
||||
Toggle("Channel 7 Included", isOn: $includeChannel7)
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
.disabled(channel.role == 1)
|
||||
Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)")
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(0)
|
||||
}
|
||||
}
|
||||
|
||||
let qrImage = qrCodeImage.generateQRCode(from: channelsUrl)
|
||||
VStack {
|
||||
if node != nil {
|
||||
ShareLink("Share QR Code & Link",
|
||||
item: Image(uiImage: qrImage),
|
||||
subject: Text("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you"),
|
||||
message: Text(channelsUrl),
|
||||
preview: SharePreview("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you",
|
||||
image: Image(uiImage: qrImage))
|
||||
)
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
|
||||
Image(uiImage: qrImage)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(
|
||||
minWidth: smallest * 0.95,
|
||||
maxWidth: smallest * 0.95,
|
||||
minHeight: smallest * 0.95,
|
||||
maxHeight: smallest * 0.95,
|
||||
alignment: .top
|
||||
)
|
||||
Button {
|
||||
isPresentingHelp = true
|
||||
} label: {
|
||||
Label("Help Me!", systemImage: "lifepreserver")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.small)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let qrImage = qrCodeImage.generateQRCode(from: channelsUrl)
|
||||
VStack {
|
||||
if node != nil {
|
||||
ShareLink("Share QR Code & Link",
|
||||
item: Image(uiImage: qrImage),
|
||||
subject: Text("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you"),
|
||||
message: Text(channelsUrl),
|
||||
preview: SharePreview("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you",
|
||||
image: Image(uiImage: qrImage))
|
||||
)
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
|
||||
Image(uiImage: qrImage)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(
|
||||
minWidth: smallest * 0.95,
|
||||
maxWidth: smallest * 0.95,
|
||||
minHeight: smallest * 0.95,
|
||||
maxHeight: smallest * 0.95,
|
||||
alignment: .top
|
||||
)
|
||||
Button {
|
||||
isPresentingHelp = true
|
||||
} label: {
|
||||
Label("Help Me!", systemImage: "lifepreserver")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isPresentingHelp) {
|
||||
VStack {
|
||||
Text("Meshtastic Channels").font(.title)
|
||||
Text("A Meshtastic LoRa Mesh network can have up to 8 distinct channels.")
|
||||
.font(.headline)
|
||||
.padding(.bottom)
|
||||
Text("Primary Channel").font(.title2)
|
||||
Text("The first channel is the Primary channel and is where much of the mesh activity takes place. DM's are only available on the primary channel and it can not be disabled.")
|
||||
.font(.callout)
|
||||
.padding([.leading,.trailing,.bottom])
|
||||
Text("Admin Channel").font(.title2)
|
||||
Text("A channel with the name 'admin' is the Admin channel and can be used to remotely administer nodes on your mesh, text messages can not be sent over the admin channel.")
|
||||
.font(.callout)
|
||||
.padding([.leading,.trailing,.bottom])
|
||||
Text("Private Channels").font(.title2)
|
||||
Text("The other channels can be used for private group converations. Each of these groups has its own encryption key.")
|
||||
.font(.callout)
|
||||
.padding([.leading,.trailing,.bottom])
|
||||
Divider()
|
||||
}
|
||||
.padding()
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.automatic)
|
||||
}
|
||||
.navigationTitle("Generate QR Code")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
|
||||
})
|
||||
.onAppear {
|
||||
bleManager.context = context
|
||||
GenerateChannelSet()
|
||||
}
|
||||
.onChange(of: includeChannel1) { includeCh1 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel2) { includeCh2 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel3) { includeCh3 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel4) { includeCh4 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel5) { includeCh5 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel6) { includeCh6 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel7) { includeCh7 in GenerateChannelSet() }
|
||||
|
||||
//}
|
||||
}
|
||||
.sheet(isPresented: $isPresentingHelp) {
|
||||
VStack {
|
||||
Text("Meshtastic Channels").font(.title)
|
||||
Text("A Meshtastic LoRa Mesh network can have up to 8 distinct channels.")
|
||||
.font(.headline)
|
||||
.padding(.bottom)
|
||||
Text("Primary Channel").font(.title2)
|
||||
Text("The first channel is the Primary channel and is where much of the mesh activity takes place. DM's are only available on the primary channel and it can not be disabled.")
|
||||
.font(.callout)
|
||||
.padding([.leading,.trailing,.bottom])
|
||||
Text("Admin Channel").font(.title2)
|
||||
Text("A channel with the name 'admin' is the Admin channel and can be used to remotely administer nodes on your mesh, text messages can not be sent over the admin channel.")
|
||||
.font(.callout)
|
||||
.padding([.leading,.trailing,.bottom])
|
||||
Text("Private Channels").font(.title2)
|
||||
Text("The other channels can be used for private group converations. Each of these groups has its own encryption key.")
|
||||
.font(.callout)
|
||||
.padding([.leading,.trailing,.bottom])
|
||||
Divider()
|
||||
}
|
||||
.padding()
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.automatic)
|
||||
}
|
||||
.navigationTitle("Generate QR Code")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
|
||||
})
|
||||
.onAppear {
|
||||
bleManager.context = context
|
||||
GenerateChannelSet()
|
||||
}
|
||||
.onChange(of: includeChannel1) { includeCh1 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel2) { includeCh2 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel3) { includeCh3 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel4) { includeCh4 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel5) { includeCh5 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel6) { includeCh6 in GenerateChannelSet() }
|
||||
.onChange(of: includeChannel7) { includeCh7 in GenerateChannelSet() }
|
||||
}
|
||||
// }
|
||||
}
|
||||
func GenerateChannelSet() {
|
||||
channelSet = ChannelSet()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue