mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge from main
This commit is contained in:
commit
3672132efd
16 changed files with 1068 additions and 679 deletions
|
|
@ -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";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
73
MeshtasticClient/Protobufs/apponly.pb.swift
Normal file
73
MeshtasticClient/Protobufs/apponly.pb.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 : "???")
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue