Merge from main

This commit is contained in:
Joshua Pirihi 2022-01-10 06:35:42 +13:00
commit 3672132efd
16 changed files with 1068 additions and 679 deletions

View file

@ -718,6 +718,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\"";
DEVELOPMENT_TEAM = 37C534H572;
ENABLE_PREVIEWS = YES;
@ -727,11 +728,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6";
};
@ -745,6 +746,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\"";
DEVELOPMENT_TEAM = 37C534H572;
ENABLE_PREVIEWS = YES;
@ -754,11 +756,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.42;
MARKETING_VERSION = 1.43;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6";
};

View file

@ -2,6 +2,7 @@ import Foundation
import CoreData
import CoreBluetooth
import SwiftUI
import MapKit
// ---------------------------------------------------------------------------------------
// Meshtastic BLE Device Manager
@ -27,6 +28,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
@Published var connectedPeripheral: Peripheral!
//@Published var lastConnectedPeripheral: String
@Published var lastConnectionError: String
@Published var lastConnnectionVersion: String
@Published var isSwitchedOn: Bool = false
@Published var isScanning: Bool = false
@ -54,8 +56,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
override init() {
self.meshLoggingEnabled = true // UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? true
//self.lastConnectedPeripheral = ""
self.lastConnectionError = ""
self.lastConnnectionVersion = "0.0.0"
super.init()
// let bleQueue: DispatchQueue = DispatchQueue(label: "CentralManager")
centralManager = CBCentralManager(delegate: self, queue: nil)
@ -419,19 +421,52 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
myInfo.myNodeNum = Int64(decodedInfo.myInfo.myNodeNum)
myInfo.hasGps = decodedInfo.myInfo.hasGps_p
myInfo.numBands = Int32(bitPattern: decodedInfo.myInfo.numBands)
myInfo.firmwareVersion = decodedInfo.myInfo.firmwareVersion
// Swift does strings weird, this does work
let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".")//.lastIndex(of: ".", offsetBy: -1)
var version = decodedInfo.myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(encodedOffset:6))]
version = version.dropLast()
myInfo.firmwareVersion = String(version)
lastConnnectionVersion = String(version)
myInfo.messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec)
myInfo.minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion)
myInfo.maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels)
connectedPeripheral.num = myInfo.myNodeNum
connectedPeripheral.firmwareVersion = myInfo.firmwareVersion ?? "Unknown"
let fetchBCUserRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "UserEntity")
fetchBCUserRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.myInfo.myNodeNum))
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")
}
} catch {
print("💥 Error Saving the All - Broadcast User")
}
} else {
fetchedMyInfo[0].myNodeNum = Int64(decodedInfo.myInfo.myNodeNum)
fetchedMyInfo[0].hasGps = decodedInfo.myInfo.hasGps_p
fetchedMyInfo[0].numBands = Int32(bitPattern: decodedInfo.myInfo.numBands)
fetchedMyInfo[0].firmwareVersion = decodedInfo.myInfo.firmwareVersion
let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".")//.lastIndex(of: ".", offsetBy: -1)
var version = decodedInfo.myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(encodedOffset:6))]
version = version.dropLast()
fetchedMyInfo[0].firmwareVersion = String(version)
lastConnnectionVersion = String(version)
fetchedMyInfo[0].messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec)
fetchedMyInfo[0].minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion)
fetchedMyInfo[0].maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels)
@ -471,7 +506,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
let newNode = NodeInfoEntity(context: context!)
newNode.id = Int64(decodedInfo.nodeInfo.num)
newNode.num = Int64(decodedInfo.nodeInfo.num)
if decodedInfo.nodeInfo.lastHeard != nil && decodedInfo.nodeInfo.lastHeard > 0 {
if decodedInfo.nodeInfo.lastHeard > 0 {
newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.nodeInfo.lastHeard)))
}
else {
@ -706,7 +741,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
fetchedNode[0].id = Int64(decodedInfo.packet.from)
fetchedNode[0].num = Int64(decodedInfo.packet.from)
if decodedInfo.packet.rxTime != nil && decodedInfo.packet.rxTime > 0 {
if decodedInfo.packet.rxTime > 0 {
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime)))
}
else {
@ -786,20 +821,27 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
print("💥 Error Fetching NodeInfoEntity for NODEINFO_APP")
}
} else if decodedInfo.packet.decoded.portnum == PortNum.adminApp {
//
} else if decodedInfo.packet.decoded.portnum == PortNum.storeForwardApp {
if meshLoggingEnabled { MeshLogger.log("🚨 MESH PACKET received for Store Forward App UNHANDLED \(try decodedInfo.packet.jsonString())") }
print(" MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())")
} else if decodedInfo.packet.decoded.portnum == PortNum.adminApp {
if meshLoggingEnabled { MeshLogger.log("🚨 MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())") }
print("🚨 MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())")
print(" MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())")
} else if decodedInfo.packet.decoded.portnum == PortNum.routingApp {
if meshLoggingEnabled { MeshLogger.log("🚨 MESH PACKET received for Routing App UNHANDLED \(try decodedInfo.packet.jsonString())") }
print("🚨 MESH PACKET received for Routing App UNHANDLED \(try decodedInfo.packet.jsonString())")
print(" MESH PACKET received for Routing App UNHANDLED \(try decodedInfo.packet.jsonString())")
} else {
if meshLoggingEnabled { MeshLogger.log("🚨 MESH PACKET received for Other App UNHANDLED \(try decodedInfo.packet.jsonString())") }
print("🚨 MESH PACKET received for Other App UNHANDLED \(try decodedInfo.packet.jsonString())")
print(" MESH PACKET received for Other App UNHANDLED \(try decodedInfo.packet.jsonString())")
}
} catch {
@ -823,8 +865,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
peripheral.readValue(for: FROMRADIO_characteristic)
}
// Send Broadcast Message
public func sendMessage(message: String, toUserNum: Int64) -> Bool {
// Send Message
public func sendMessage(message: String, toUserNum: Int64, isTapback: Bool, replyID: Int64) -> Bool {
var success = false
@ -839,18 +881,11 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil {
connectTo(peripheral: preferredPeripheral!.peripheral)
}
// else {
//
// // Try and connect to the last connected device
// let lastConnectedPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == self.lastConnectedPeripheral }).first
// if lastConnectedPeripheral != nil && lastConnectedPeripheral?.peripheral != nil {
// connectTo(peripheral: lastConnectedPeripheral!.peripheral)
// }
// }
//print("🚫 Message Send Failed, not properly connected to \(lastConnectedPeripheral)")
//if meshLoggingEnabled { MeshLogger.log("🚫 Message Send Failed, not properly connected to \(lastConnectedPeripheral)") }
print("🚫 Message Send Failed, not properly connected to \(preferredPeripheral?.name ?? "Unknown")")
if meshLoggingEnabled { MeshLogger.log("🚫 Message Send Failed, not properly connected to \(preferredPeripheral?.name ?? "Unknown")") }
success = false
} else if message.count < 1 {
// Don't send an empty message
@ -880,6 +915,12 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
newMessage.receivedACK = false
newMessage.direction = "IN"
newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum })
newMessage.isTapback = isTapback
if replyID > 0 {
newMessage.replyID = replyID
}
if newMessage.toUser == nil {
let bcu: UserEntity = UserEntity(context: context!)
@ -890,6 +931,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
bcu.userId = "BROADCASTNODE"
newMessage.toUser = bcu
}
newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum })
newMessage.messagePayload = message
@ -903,6 +945,9 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
var meshPacket = MeshPacket()
meshPacket.to = UInt32(toUserNum)
meshPacket.from = UInt32(fromUserNum)
if replyID > 0 {
meshPacket.replyID = UInt32(replyID)
}
meshPacket.decoded = dataMessage
meshPacket.wantAck = true

View file

@ -39,6 +39,22 @@ extension String {
return data
}
func image(fontSize:CGFloat = 40, bgColor:UIColor = UIColor.clear, imageSize:CGSize? = nil) -> UIImage?
{
let font = UIFont.systemFont(ofSize: fontSize)
let attributes = [NSAttributedString.Key.font: font]
let imageSize = imageSize ?? self.size(withAttributes: attributes)
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
bgColor.set()
let rect = CGRect(origin: .zero, size: imageSize)
UIRectFill(rect)
self.draw(in: rect, withAttributes: [.font: font])
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}

View file

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>

View file

@ -2,7 +2,7 @@
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21C52" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
<attribute name="direction" attributeType="String"/>
<attribute name="isTapback" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isTapback" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="messagePayload" attributeType="String"/>
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
@ -10,6 +10,9 @@
<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"/>
<fetchedProperty name="tapbacks" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isTapback == true"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="messageId"/>
@ -65,12 +68,15 @@
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
<fetchedProperty name="allMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND isTapback == false"/>
</fetchedProperty>
</entity>
<elements>
<element name="MessageEntity" positionX="-36" positionY="63" width="128" height="164"/>
<element name="MessageEntity" positionX="-36" positionY="63" width="128" height="185"/>
<element name="MyInfoEntity" positionX="-18" positionY="81" width="128" height="149"/>
<element name="NodeInfoEntity" positionX="-63" positionY="-18" width="128" height="149"/>
<element name="PositionEntity" positionX="-54" positionY="54" width="128" height="119"/>
<element name="UserEntity" positionX="0" positionY="144" width="128" height="179"/>
<element name="UserEntity" positionX="0" positionY="144" width="128" height="200"/>
</elements>
</model>

View file

@ -34,9 +34,11 @@ class PersistenceController {
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Meshtastic")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (_, error) in
// Merge policy that favors in memory data over data in the db
@ -44,17 +46,8 @@ class PersistenceController {
self.container.viewContext.automaticallyMergesChangesFromParent = true
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
print("💥 CoreData Error: \(error.localizedDescription). Now attempting to truncate CoreData database. All app data will be lost.")
self.clearDatabase()
}
})

View file

@ -0,0 +1,73 @@
// DO NOT EDIT.
// swift-format-ignore-file
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: apponly.proto
//
// For information on using the generated types, please see the documentation:
// https://github.com/apple/swift-protobuf/
import Foundation
import SwiftProtobuf
// If the compiler emits an error on this type, it is because this file
// was generated by a version of the `protoc` Swift plug-in that is
// incompatible with the version of SwiftProtobuf to which you are linking.
// Please ensure that you are building against the same version of the API
// that was used to generate this file.
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
typealias Version = _2
}
///
/// This is the most compact possible representation for a set of channels.
/// It includes only one PRIMARY channel (which must be first) and
/// any SECONDARY channels.
/// No DISABLED channels are included.
/// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL
struct ChannelSet {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
var settings: [ChannelSettings] = []
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
// MARK: - Code below here is support for the SwiftProtobuf runtime.
extension ChannelSet: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "ChannelSet"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "settings"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeRepeatedMessageField(value: &self.settings) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if !self.settings.isEmpty {
try visitor.visitRepeatedMessageField(value: self.settings, fieldNumber: 1)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: ChannelSet, rhs: ChannelSet) -> Bool {
if lhs.settings != rhs.settings {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}

View file

@ -22,13 +22,28 @@ struct Connect: View {
@State var isPreferredRadio: Bool = false
var body: some View {
let firmwareVersion = bleManager.lastConnnectionVersion
let minimumVersion = "1.2.30"
let supportedVersion = firmwareVersion == "0.0.0" || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedSame
NavigationView {
VStack {
if bleManager.isSwitchedOn {
List {
if supportedVersion == false {
Section(header: Text("Upgrade your Firmware").font(.title)) {
Text("🚨 Your firmware version is unsupported, the minimum firmware version is \(minimumVersion).").font(.subheadline).foregroundColor(.red)
}
.textCase(nil)
}
if bleManager.lastConnectionError.count > 0 {
Section(header: Text("Connection Error").font(.title)) {
@ -204,9 +219,9 @@ struct Connect: View {
ZStack {
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName :
bluetoothOn: self.bleManager.isSwitchedOn,
deviceConnected: self.bleManager.connectedPeripheral != nil,
name: (bleManager.connectedPeripheral != nil) ? self.bleManager.connectedPeripheral.shortName :
"???")
}
)
@ -216,7 +231,7 @@ struct Connect: View {
self.bleManager.context = context
if bleManager.connectedPeripheral != nil && userSettings.preferredPeripheralId == bleManager.connectedPeripheral.peripheral.identifier.uuidString {
if self.bleManager.connectedPeripheral != nil && userSettings.preferredPeripheralId == self.bleManager.connectedPeripheral.peripheral.identifier.uuidString {
isPreferredRadio = true
} else {
isPreferredRadio = false

View file

@ -19,21 +19,21 @@ struct ContentView: View {
var body: some View {
TabView(selection: $selection) {
// Contacts()
// .tabItem {
// Label("Messages", systemImage: "text.bubble")
// .symbolRenderingMode(.hierarchical)
// .symbolVariant(.none)
//
// }
// .tag(Tab.contacts)
Channels()
.tabItem {
Label("Messages", systemImage: "text.bubble")
Contacts()
.tabItem {
Label("Messages", systemImage: "text.bubble")
.symbolRenderingMode(.hierarchical)
.symbolVariant(.none)
}
.tag(Tab.messages)
.symbolVariant(.none)
}
.tag(Tab.contacts)
// Channels()
// .tabItem {
// Label("Messages", systemImage: "text.bubble")
// .symbolRenderingMode(.hierarchical)
// .symbolVariant(.none)
// }
// .tag(Tab.messages)
Connect()
.tabItem {
Label("Bluetooth", systemImage: "dot.radiowaves.left.and.right")

View file

@ -8,14 +8,19 @@ import SwiftUI
struct CircleText: View {
var text: String
var color: Color
var circleSize: CGFloat? = 50
var fontSize: CGFloat? = 24
var body: some View {
let font = Font.system(size: fontSize!)
ZStack {
Circle()
.fill(color)
.frame(width: 36, height: 36)
Text(text).textCase(.uppercase).font(.caption2).foregroundColor(.white)
.frame(width: 36, height: 36, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/).offset(x: 0, y: 0)
.frame(width: circleSize, height: circleSize)
Text(text).textCase(.uppercase).font(font).foregroundColor(.white)
.frame(width: circleSize, height: circleSize, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/).offset(x: 0, y: 0)
}
}
}

View file

@ -1,44 +1,44 @@
import Foundation
import SwiftUI
import CoreBluetooth
struct Channels: View {
@State private var isShowingDetailView = true
var body: some View {
NavigationView {
NavigationLink(destination: Messages(), isActive: $isShowingDetailView) {
List {
HStack {
Image(systemName: "megaphone.fill")
.font(.largeTitle)
.symbolRenderingMode(.hierarchical)
.padding(.trailing)
.foregroundColor(.accentColor)
Text("All - Broadcast")
.font(.largeTitle)
}.padding()
}
}
.navigationTitle("Contacts")
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
struct MessageList_Previews: PreviewProvider {
static var previews: some View {
Group {
Channels()
}
}
}
//import Foundation
//import SwiftUI
//import CoreBluetooth
//
//struct Channels: View {
//
// @State private var isShowingDetailView = true
//
// var body: some View {
//
// NavigationView {
//
// NavigationLink(destination: Messages(), isActive: $isShowingDetailView) {
//
// List {
//
// HStack {
//
// Image(systemName: "megaphone.fill")
// .font(.largeTitle)
// .symbolRenderingMode(.hierarchical)
// .padding(.trailing)
// .foregroundColor(.accentColor)
//
// Text("All - Broadcast")
// .font(.largeTitle)
//
// }.padding()
// }
// }
// .navigationTitle("Contacts")
// }
// .navigationViewStyle(DoubleColumnNavigationViewStyle())
// }
//}
//
//struct MessageList_Previews: PreviewProvider {
//
// static var previews: some View {
// Group {
// Channels()
// }
// }
//}

View file

@ -1,141 +1,137 @@
////
//// Contacts.swift
//// MeshtasticClient
////
//// Created by Garth Vander Houwen on 12/21/21.
////
//
//import SwiftUI
// Contacts.swift
// MeshtasticClient
//
//struct Contacts: View {
// Created by Garth Vander Houwen on 12/21/21.
//
// @Environment(\.managedObjectContext) var context
// @EnvironmentObject var bleManager: BLEManager
//
// @FetchRequest(
// sortDescriptors: [NSSortDescriptor(key: "longName", ascending: true)],
// animation: .default)
//
// private var users: FetchedResults<UserEntity>
//
// var body: some View {
//
// NavigationView {
//
// List(users) { (user: UserEntity) in
//
// if user.receivedMessages?.count ?? 0 > 0 {
//
// let currentUserNum = self.bleManager.connectedPeripheral != nil ? self.bleManager.connectedPeripheral.num : 0
//
// let mostRecentBC = user.receivedMessages?.array.last as! MessageEntity
//
// let mostRecentDM = user.receivedMessages?.array.last(where: {($0 as! MessageEntity).toUser!.num == currentUserNum }) as? MessageEntity
//
// let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64(mostRecentDM?.messageTimestamp ?? mostRecentBC.messageTimestamp)))
// let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0
// let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0
//
// if user.num == bleManager.broadcastNodeNum {//user.num != currentUserNum && (user.num == bleManager.broadcastNodeNum || mostRecentDM != nil) {
//
// NavigationLink(destination: UserMessageList(user: user)) {
//
// HStack {
//
// VStack {
//
// CircleText(text: user.shortName ?? "???", color: Color.blue)
// }
// .padding([.leading, .trailing])
//
// VStack {
//
// HStack {
//
// VStack {
//
// Text(user.longName ?? "Unknown").font(.headline).fixedSize()
// }
//
// VStack {
//
// if lastMessageDay == currentDay {
//
// Text(lastMessageTime, style: .time )
// .font(.caption)
// .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)
//
// } else {
//
// Text(lastMessageTime, style: .date)
// }
// }.frame(maxWidth: .infinity, alignment: .trailing)
// }
// .listRowSeparator(.hidden).frame(height: 5)
//
// HStack(alignment: .top) {
// Text(mostRecentDM != nil ? mostRecentDM?.messagePayload as! String : (mostRecentBC.messagePayload ?? "Unknown" ))
// .frame(height: 60)
// .truncationMode(.tail)
// .foregroundColor(Color.gray)
// .frame(maxWidth: .infinity, alignment: .leading)
// }
// }.padding(.top, 15)
// }
// }
// }
//
// } else if false {// self.bleManager.connectedPeripheral == nil || ((self.bleManager.connectedPeripheral != nil ? self.bleManager.connectedPeripheral.num : 0) != user.num) {
//
// NavigationLink(destination: UserMessageList(user: user)) {
//
// HStack {
//
// VStack {
//
// CircleText(text: user.shortName ?? "???", color: Color.blue)
// }
// .padding(.trailing)
//
// VStack {
//
// HStack {
//
// VStack {
//
// Text(user.longName ?? "Unknown").font(.headline).fixedSize()
// }
//
// VStack {
// Text(" ")
// }
// .frame(maxWidth: .infinity, alignment: .trailing)
// }
// .listRowSeparator(.hidden).frame(height: 5)
// }
// }.padding()
// }
// }
// }
// .navigationTitle("Contacts")
// .navigationBarTitleDisplayMode(.inline)
// }
// .listStyle(PlainListStyle())
// }
//}
//
//struct Contacts_Previews: PreviewProvider {
// static var previews: some View {
// Contacts()
// }
//}
import SwiftUI
struct Contacts: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "longName", ascending: true)],
animation: .default)
private var users: FetchedResults<UserEntity>
var body: some View {
NavigationView {
List(users) { (user: UserEntity) in
let currentUserNum = self.bleManager.connectedPeripheral != nil ? self.bleManager.connectedPeripheral.num : 0
let allMessages = user.value(forKey: "allMessages") as! [MessageEntity]
NavigationLink(destination: UserMessageList(user: user)) {
if allMessages.count > 0 {
let mostRecent = allMessages.last
let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent!.messageTimestamp ))))
let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0
let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0
HStack {
VStack {
CircleText(text: user.shortName ?? "???", color: Color.blue)
}
.padding([.leading, .trailing])
VStack {
HStack {
VStack {
Text(user.longName ?? "Unknown").font(.headline).fixedSize()
}
VStack {
if lastMessageDay == currentDay {
Text(lastMessageTime, style: .time )
.font(.caption)
.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)
} else {
Text(lastMessageTime, style: .date)
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.listRowSeparator(.hidden).frame(height: 5)
HStack(alignment: .top) {
Text(mostRecent!.messagePayload ?? "Empty Message")
.frame(height: 60)
.truncationMode(.tail)
.foregroundColor(Color.gray)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(.top)
}
} else {
HStack {
VStack {
CircleText(text: user.shortName ?? "???", color: Color.blue)
}
.padding(.trailing)
VStack {
HStack {
VStack {
Text(user.longName ?? "Unknown").font(.headline).fixedSize()
}
VStack {
Text(" ")
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.listRowSeparator(.hidden).frame(height: 5)
}
}.padding()
}
}
}
.navigationTitle("Contacts")
.navigationBarTitleDisplayMode(.inline)
}
.listStyle(PlainListStyle())
}
}
struct Contacts_Previews: PreviewProvider {
static var previews: some View {
Contacts()
}
}

View file

@ -1,204 +1,204 @@
import SwiftUI
import MapKit
import Foundation
import CoreLocation
struct Messages: View {
enum Field: Hashable {
case messageText
}
// CoreData
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \MessageEntity.messageTimestamp, ascending: true)],
animation: .default)
var messages: FetchedResults<MessageEntity>
// Keyboard State
@State var typingMessage: String = ""
@State private var totalBytes = 0
private var maxbytes = 228
@State private var lastTypingMessage = ""
@FocusState private var focusedField: Field?
@State var showDeleteMessageAlert = false
@State private var deleteMessageId: Int64 = 0
public var broadcastNodeId: UInt32 = 4294967295
var body: some View {
Text("\(messages.count) Messages").font(.caption)
GeometryReader { bounds in
VStack {
ScrollViewReader { scrollView in
if self.messages.count > 0 {
ScrollView {
ForEach(messages) { message in
HStack(alignment: .top) {
let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false )
CircleText(text: (message.fromUser?.shortName ?? "???"), color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5)
.gesture(LongPressGesture(minimumDuration: 2)
.onEnded {_ in
print(messages)
print("I want to delete message: \(message.messageId)")
self.showDeleteMessageAlert = true
self.deleteMessageId = message.messageId
print(deleteMessageId)
})
VStack(alignment: .leading) {
Text(message.messagePayload ?? "EMPTY MESSAGE")
.textSelection(.enabled)
.padding(10)
.foregroundColor(.white)
.background(currentUser ? Color.blue : Color(.darkGray))
.cornerRadius(10)
HStack(spacing: 4) {
let time = Int32(message.messageTimestamp)
let messageDate = Date(timeIntervalSince1970: TimeInterval(time))
if time != 0 {
Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray)
Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray)
} else {
Text("Unknown").font(.caption2).foregroundColor(.gray)
}
}
.padding(.bottom, 10)
}
Spacer()
}
.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 = messages.first(where: { $0.messageId == deleteMessageId })
context.delete(message!)
do {
try context.save()
deleteMessageId = 0
} catch {
print("Failed to delete message \(deleteMessageId)")
}
}
},
secondaryButton: .cancel()
)
}
}
.onChange(of: messages.count, perform: { newValue in
if messages.count > 0 {
scrollView.scrollTo(messages[messages.count-1].id, anchor: .bottom)
}
}
)
.onAppear(perform: {
self.bleManager.context = context
if messages.count > 0 {
scrollView.scrollTo(messages[messages.count-1].id, anchor: .bottom)
}
})
}
.padding(.horizontal)
}
}
HStack(alignment: .top) {
ZStack {
let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0)
TextEditor(text: $typingMessage)
.onChange(of: typingMessage, perform: { value in
let size = value.utf8.count
totalBytes = size
if totalBytes <= maxbytes {
// Allow the user to type
lastTypingMessage = typingMessage
} else {
// Set the message back and remove the bytes over the count
self.typingMessage = lastTypingMessage
}
})
.keyboardType(kbType!)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Dismiss Keyboard") {
focusedField = nil
}
.font(.subheadline)
Spacer()
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: bounds.size.height / 4, maxHeight: bounds.size.height / 4)
Text(typingMessage).opacity(0).padding(.all, 0)
}
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
.padding(.bottom, 15)
Button(action: {
if self.bleManager.sendMessage(message: typingMessage, toUserNum: Int64(self.bleManager.broadcastNodeNum)) {
typingMessage = ""
focusedField = nil
}
}) {
Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue)
}
}
.padding(.all, 15)
}
}
.navigationTitle("All - Broadcast")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(
bluetoothOn: self.bleManager.isSwitchedOn,
deviceConnected: self.bleManager.connectedPeripheral != nil,
name: (self.bleManager.connectedPeripheral != nil) ? self.bleManager.connectedPeripheral.shortName : "???")
}
)
}
}
//import SwiftUI
//import MapKit
//import Foundation
//import CoreLocation
//
//struct Messages: View {
//
// enum Field: Hashable {
// case messageText
// }
//
// // CoreData
// @Environment(\.managedObjectContext) var context
// @EnvironmentObject var bleManager: BLEManager
//
// @FetchRequest(
// sortDescriptors: [NSSortDescriptor(keyPath: \MessageEntity.messageTimestamp, ascending: true)],
// animation: .default)
// var messages: FetchedResults<MessageEntity>
//
// // Keyboard State
// @State var typingMessage: String = ""
// @State private var totalBytes = 0
// private var maxbytes = 228
// @State private var lastTypingMessage = ""
// @FocusState private var focusedField: Field?
//
// @State var showDeleteMessageAlert = false
// @State private var deleteMessageId: Int64 = 0
//
// public var broadcastNodeId: UInt32 = 4294967295
//
// var body: some View {
//
// Text("\(messages.count) Messages").font(.caption)
// GeometryReader { bounds in
//
// VStack {
//
// ScrollViewReader { scrollView in
//
// if self.messages.count > 0 {
//
// ScrollView {
//
// ForEach(messages) { message in
//
// HStack(alignment: .top) {
// let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false )
//
// CircleText(text: (message.fromUser?.shortName ?? "???"), color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5)
// .gesture(LongPressGesture(minimumDuration: 2)
// .onEnded {_ in
// print(messages)
// print("I want to delete message: \(message.messageId)")
// self.showDeleteMessageAlert = true
// self.deleteMessageId = message.messageId
//
// print(deleteMessageId)
// })
//
// VStack(alignment: .leading) {
// Text(message.messagePayload ?? "EMPTY MESSAGE")
// .textSelection(.enabled)
// .padding(10)
// .foregroundColor(.white)
// .background(currentUser ? Color.blue : Color(.darkGray))
// .cornerRadius(10)
// HStack(spacing: 4) {
//
// let time = Int32(message.messageTimestamp)
// let messageDate = Date(timeIntervalSince1970: TimeInterval(time))
//
// if time != 0 {
// Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray)
// Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray)
// } else {
// Text("Unknown").font(.caption2).foregroundColor(.gray)
// }
// }
// .padding(.bottom, 10)
// }
// Spacer()
// }
// .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 = messages.first(where: { $0.messageId == deleteMessageId })
//
// context.delete(message!)
// do {
// try context.save()
//
// deleteMessageId = 0
//
// } catch {
// print("Failed to delete message \(deleteMessageId)")
// }
//
// }
// },
// secondaryButton: .cancel()
// )
// }
// }
// .onChange(of: messages.count, perform: { newValue in
//
// if messages.count > 0 {
//
// scrollView.scrollTo(messages[messages.count-1].id, anchor: .bottom)
// }
// }
// )
// .onAppear(perform: {
//
// self.bleManager.context = context
// if messages.count > 0 {
//
// scrollView.scrollTo(messages[messages.count-1].id, anchor: .bottom)
// }
// })
// }
// .padding(.horizontal)
// }
// }
//
// HStack(alignment: .top) {
//
// ZStack {
//
// let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0)
// TextEditor(text: $typingMessage)
// .onChange(of: typingMessage, perform: { value in
//
// let size = value.utf8.count
// totalBytes = size
// if totalBytes <= maxbytes {
// // Allow the user to type
// lastTypingMessage = typingMessage
// } else {
// // Set the message back and remove the bytes over the count
// self.typingMessage = lastTypingMessage
// }
// })
// .keyboardType(kbType!)
// .toolbar {
// ToolbarItemGroup(placement: .keyboard) {
//
// Button("Dismiss Keyboard") {
// focusedField = nil
// }
// .font(.subheadline)
//
// Spacer()
//
// 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: bounds.size.height / 4, maxHeight: bounds.size.height / 4)
//
// Text(typingMessage).opacity(0).padding(.all, 0)
//
// }
// .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
// .padding(.bottom, 15)
//
// Button(action: {
// if self.bleManager.sendMessage(message: typingMessage, toUserNum: Int64(self.bleManager.broadcastNodeNum)) {
// typingMessage = ""
// focusedField = nil
// }
//
// }) {
// Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue)
// }
//
// }
// .padding(.all, 15)
// }
// }
// .navigationTitle("All - Broadcast")
// .navigationBarTitleDisplayMode(.inline)
// .navigationBarItems(trailing:
//
// ZStack {
//
// ConnectedDevice(
// bluetoothOn: self.bleManager.isSwitchedOn,
// deviceConnected: self.bleManager.connectedPeripheral != nil,
// name: (self.bleManager.connectedPeripheral != nil) ? self.bleManager.connectedPeripheral.shortName : "???")
// }
// )
// }
//}

View file

@ -1,206 +1,423 @@
////
//// UserMessageList.swift
//// MeshtasticClient
////
//// Created by Garth Vander Houwen on 12/24/21.
////
//
//import SwiftUI
//import CoreData
// UserMessageList.swift
// MeshtasticClient
//
//struct UserMessageList: View {
// Created by Garth Vander Houwen on 12/24/21.
//
// @Environment(\.managedObjectContext) var context
// @EnvironmentObject var bleManager: BLEManager
//
// enum Field: Hashable {
// case messageText
// }
// // Keyboard State
// @State var typingMessage: String = ""
// @State private var totalBytes = 0
// var maxbytes = 228
// @State var lastTypingMessage = ""
// @FocusState var focusedField: Field?
//
// var user: UserEntity
//
// @State var showDeleteMessageAlert = false
// @State private var deleteMessageId: Int64 = 0
//
// var body: some View {
//
//// HStack {
//
// VStack {
//
// // List {
//
// ScrollViewReader { _ in
//
// ScrollView {
//
// if user.receivedMessages?.count ?? 0 > 0 {
//
// ForEach( user.receivedMessages?.array as! [MessageEntity], id: \.self) { (message: MessageEntity) in
//
// // HStack {
// let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false )
//
//
// if message.toUser!.num == Int64(bleManager.broadcastNodeNum) || ((bleManager.connectedPeripheral) != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : true {
//
//
// HStack (alignment: .top) {
//
// if currentUser { Spacer(minLength:50) }
//
// if !currentUser {
//
// CircleText(text: (message.fromUser?.shortName ?? "???"), color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5)
// .gesture(LongPressGesture(minimumDuration: 2).onEnded {_ in
//
// print("I want to delete message: \(message.messageId)")
// self.showDeleteMessageAlert = true
// self.deleteMessageId = message.messageId
// print(deleteMessageId)
// })
// }
//
// VStack(alignment: currentUser ? .trailing : .leading) {
//
// Text(message.messagePayload ?? "EMPTY MESSAGE")
// .textSelection(.enabled)
// .padding(10)
// .foregroundColor(.white)
// .background(currentUser ? Color.blue : Color(.darkGray))
// .cornerRadius(15)
//
// HStack(spacing: 4) {
//
// let time = Int32(message.messageTimestamp)
// let messageDate = Date(timeIntervalSince1970: TimeInterval(time))
//
// if time != 0 {
// Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray)
// Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray)
// } else {
// Text("Unknown").font(.caption2).foregroundColor(.gray)
// }
// }
// .padding(.bottom, 10)
// }
// if !currentUser {
// Spacer(minLength:50)
// }
// }
// .padding(.trailing)
// .frame(maxWidth: .infinity)
// }
// // }
// }
// .listRowSeparator(.hidden)
// }
// }
// }
// // }
// // .padding(.top)
//
// HStack(alignment: .top) {
//
// ZStack {
//
// let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0)
// TextEditor(text: $typingMessage)
// .onChange(of: typingMessage, perform: { value in
//
// let size = value.utf8.count
// totalBytes = size
// if totalBytes <= maxbytes {
// // Allow the user to type
// lastTypingMessage = typingMessage
// } else {
// // Set the message back and remove the bytes over the count
// self.typingMessage = lastTypingMessage
// }
// })
// .keyboardType(kbType!)
// .toolbar {
// ToolbarItemGroup(placement: .keyboard) {
//
// Button("Dismiss Keyboard") {
// focusedField = nil
// }
// .font(.subheadline)
//
// Spacer()
//
// 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: 100, maxHeight: 160)
//
// 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) {
// typingMessage = ""
// focusedField = nil
// } else {
//
// _ = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { (_) in
//
// if bleManager.sendMessage(message: typingMessage, toUserNum: user.num) {
// typingMessage = ""
// }
// }
// }
//
// }) {
// 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).fixedSize()
// Text(user.longName ?? "Unknown").foregroundColor(.gray).font(.caption2).fixedSize()
// }
// }
// ToolbarItem(placement: .navigationBarTrailing) {
// ZStack {
//
// ConnectedDevice(
// bluetoothOn: bleManager.isSwitchedOn,
// deviceConnected: bleManager.connectedPeripheral != nil,
// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "???")
// }
// }
// }
// .onAppear(perform: {
//
// self.bleManager.context = context
//
// })
// }
//
//}
import SwiftUI
import CoreData
struct UserMessageList: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
enum Field: Hashable {
case messageText
}
// Keyboard State
@State var typingMessage: String = ""
@State private var totalBytes = 0
var maxbytes = 228
@State var lastTypingMessage = ""
@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 var messageCount = 0
var body: some View {
let firmwareVersion = bleManager.lastConnnectionVersion
let minimumVersion = "1.2.50"
let hasTapbackSupport = minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedSame
VStack {
let allMessages = user.value(forKey: "allMessages") as! [MessageEntity]
ScrollViewReader { scrollView in
ScrollView {
if allMessages.count > 0 {
HStack{
// Padding at the top of the message list
}.padding(.bottom)
ForEach( allMessages ) { (message: MessageEntity) in
let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false )
if message.toUser!.num == Int64(bleManager.broadcastNodeNum) || ((bleManager.connectedPeripheral) != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : true {
if message.replyID > 0 {
let messageReply = allMessages.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: 36, fontSize: 16).padding(.all, 5)
}
VStack(alignment: currentUser ? .trailing : .leading) {
Text(message.messagePayload ?? "EMPTY MESSAGE")
.padding(10)
.foregroundColor(.white)
.background(currentUser ? Color.blue : Color(.darkGray))
.cornerRadius(15)
.contextMenu {
if hasTapbackSupport {
Menu("Tapback response") {
Button(action: {
if bleManager.sendMessage(message: "❤️", toUserNum: user.num, isTapback: 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, isTapback: 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, isTapback: 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, isTapback: 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, isTapback: 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, isTapback: true, replyID: message.messageId) {
print("Sent ❓ Tapback")
self.context.refresh(user, mergeChanges: true)
} else { print("❓ Tapback Failed") }
}) {
Text("Question Mark")
let image = "".image()
Image(uiImage: image!)
}
Button(action: {
if bleManager.sendMessage(message: "💩", toUserNum: user.num, isTapback: true, replyID: message.messageId) {
print("Sent 💩 Tapback")
self.context.refresh(user, mergeChanges: true)
} 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")
}
Divider()
Button(role: .destructive, action: {
self.showDeleteMessageAlert = true
self.deleteMessageId = message.messageId
print(deleteMessageId)
}) {
Text("Delete")
Image(systemName: "trash")
}
}
if hasTapbackSupport {
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 {
let time = Int32(message.messageTimestamp)
let messageDate = Date(timeIntervalSince1970: TimeInterval(time))
if time != 0 {
Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray)
Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray)
} else {
Text("Unknown").font(.caption2).foregroundColor(.gray)
}
}
.padding(4)
}
.id(allMessages.firstIndex(of: message))
if !currentUser {
Spacer(minLength:50)
}
}
.padding([.leading, .trailing])
.frame(maxWidth: .infinity)
.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 = allMessages.first(where: { $0.messageId == deleteMessageId })
context.delete(message!)
do {
try context.save()
deleteMessageId = 0
} catch {
print("Failed to delete message \(deleteMessageId)")
}
}
},
secondaryButton: .cancel()
)
}
}
}
.listRowSeparator(.hidden)
}
}
.onAppear(perform: {
self.bleManager.context = context
messageCount = ((user.sentMessages?.count ?? 0) + (user.receivedMessages?.count ?? 0))
if messageCount > 0 {
scrollView.scrollTo(allMessages.firstIndex(of: allMessages.last! ), anchor: .bottom)
}
})
.onChange(of: allMessages.count, perform: { count in
self.context.refresh(user, mergeChanges: true)
let index = count - 1
if index > 2 {
scrollView.scrollTo(index, anchor: .bottom)
}
})
}
HStack(alignment: .top) {
ZStack {
let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0)
TextEditor(text: $typingMessage)
.onChange(of: typingMessage, perform: { value in
let size = value.utf8.count
totalBytes = size
if totalBytes <= maxbytes {
// Allow the user to type
lastTypingMessage = typingMessage
} else {
// Set the message back and remove the bytes over the count
self.typingMessage = lastTypingMessage
}
})
.keyboardType(kbType!)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Dismiss Keyboard") {
focusedField = nil
}
.font(.subheadline)
Spacer()
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: 100, maxHeight: 160)
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, isTapback: false, replyID: replyMessageId) {
typingMessage = ""
focusedField = nil
replyMessageId = 0
}
}) {
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: 20).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 : "???")
}
}
}
.onAppear(perform: {
self.bleManager.context = context
})
}
}

View file

@ -42,7 +42,7 @@ struct NodeDetail: View {
MapAnnotation(
coordinate: location.coordinate,
content: {
CircleText(text: node.user!.shortName ?? "???", color: .accentColor)
CircleText(text: node.user!.shortName ?? "???", color: .accentColor, circleSize: 33, fontSize: 16)
}
)
}
@ -91,13 +91,16 @@ struct NodeDetail: View {
VStack {
Image(node.user!.hwModel ?? "UNSET")
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(5)
if node.user != nil {
Image(node.user!.hwModel ?? "UNSET")
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(5)
Text(String(node.user!.hwModel ?? "UNSET"))
.font(.callout).fixedSize()
Text(String(node.user!.hwModel ?? "UNSET"))
.font(.callout).fixedSize()
}
}
.padding(5)
@ -160,7 +163,7 @@ struct NodeDetail: View {
.font(.title2)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Unique Id:").font(.title2)
Text("User Id:").font(.title2)
}
Text(node.user?.userId ?? "??????").font(.title3).foregroundColor(.gray)
}
@ -175,11 +178,18 @@ struct NodeDetail: View {
}
Text(String(node.num)).font(.title3).foregroundColor(.gray)
}
}.padding(5)
}
.padding(5)
Divider()
HStack {
Image(systemName: "globe")
.font(.headline)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("MAC Address: ")
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray)
}
.padding()
if node.positions?.count ?? 0 > 1 {
@ -187,7 +197,7 @@ struct NodeDetail: View {
HStack {
Image(systemName: "map.circle.fill")
Image(systemName: "location.circle.fill")
.font(.title)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
@ -210,8 +220,13 @@ struct NodeDetail: View {
Text("\(String(mappin.latitude ?? 0)) \(String(mappin.longitude ?? 0))")
.foregroundColor(.gray)
.font(.caption)
Text("Altitude:")
Image(systemName: "arrow.up.arrow.down.circle")
.font(.subheadline)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Alt:")
.font(.caption)
Text("\(String(mappin.altitude))m")
@ -231,21 +246,27 @@ struct NodeDetail: View {
.font(.caption)
Divider()
Text("Battery").font(.caption).fixedSize()
Text(String(mappin.batteryLevel) + "%")
.font(.caption)
.foregroundColor(.gray)
.symbolRenderingMode(.hierarchical)
HStack {
BatteryIcon(batteryLevel: mappin.batteryLevel, font: .subheadline, color: .accentColor)
if mappin.batteryLevel > 0 {
Text(String(mappin.batteryLevel) + "%")
.font(.caption2)
.foregroundColor(.gray)
}
}
}
}
.padding(1)
Divider()
}
}
.padding(.bottom, 5) // Without some padding here there is a transparent contentview bug
}
}
}.ignoresSafeArea(.all, edges: [.leading, .trailing])
}
.edgesIgnoringSafeArea([.leading, .trailing])
}
}
.navigationTitle(node.user!.longName ?? "Unknown")

View file

@ -164,7 +164,16 @@ struct AppSettings: View {
}
.pickerStyle(DefaultPickerStyle())
}
Section(header: Text("MESH NETWORK OPTIONS")) {
Section(header: Text("MAP OPTIONS")) {
Picker("Map Type", selection: $userSettings.meshMapType) {
ForEach(MeshMapType.allCases) { map in
Text(map.description)
}
}
.pickerStyle(DefaultPickerStyle())
TextField("Custom Tile Server", text: $userSettings.meshMapCustomTileServer)
}
Section(header: Text("DEBUG")) {
// Toggle(isOn: $userSettings.meshActivityLog) {
// Label("Log all Mesh activity", systemImage: "network")
@ -177,15 +186,6 @@ struct AppSettings: View {
.listRowSeparator(.visible)
}
}
Section(header: Text("MAP OPTIONS")) {
Picker("Base Map (Apple)", selection: $userSettings.meshMapType) {
ForEach(MeshMapType.allCases) { map in
Text(map.description)
}
}
.pickerStyle(DefaultPickerStyle())
TextField("Custom Tile Server", text: $userSettings.meshMapCustomTileServer)
}
}
}
.navigationTitle("App Settings")