Merge pull request #224 from meshtastic/2.0_Alpha_Launch

2.0 alpha launch
This commit is contained in:
Garth Vander Houwen 2022-11-08 13:02:50 -08:00 committed by GitHub
commit e862a863e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1160 additions and 839 deletions

View file

@ -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 */,

View file

@ -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"
}
}
}
}

View file

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

View file

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

View file

@ -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")
}

View file

@ -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 &amp;&amp; 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>

View 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]
}
}

View 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 : "????")
}
}
}
}
}

View file

@ -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")
}
}
}
}

View file

@ -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 : "????")
}
}
}
}
}

View 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 : "????")
}
}
}
}
}

View file

@ -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:

View file

@ -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")
}
}

View file

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