mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Contact list filters
This commit is contained in:
parent
e68734135b
commit
8a214d93eb
19 changed files with 173 additions and 34 deletions
|
|
@ -56,10 +56,12 @@ enum MeshMapDistances: Double, CaseIterable, Identifiable {
|
|||
case twoHundredMiles = 321869
|
||||
case fiveHundredMiles = 804672
|
||||
case oneThousandMiles = 1609000
|
||||
case fifteenHundredMiles = 2414016
|
||||
case twentyFiveHundredMiles = 4023360
|
||||
var id: Double { self.rawValue }
|
||||
var description: String {
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
return "up to \(distanceFormatter.string(fromDistance: Double(self.rawValue))) away"
|
||||
return String.localizedStringWithFormat("nodelist.filter.distance %@".localized, distanceFormatter.string(fromDistance: Double(self.rawValue)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,11 @@ extension PositionEntity {
|
|||
static func allPositionsFetchRequest() -> NSFetchRequest<PositionEntity> {
|
||||
let request: NSFetchRequest<PositionEntity> = PositionEntity.fetchRequest()
|
||||
request.fetchLimit = 100
|
||||
request.fetchBatchSize = 1
|
||||
request.returnsObjectsAsFaults = false
|
||||
request.includesSubentities = true
|
||||
request.returnsDistinctResults = true
|
||||
request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)]
|
||||
let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate)
|
||||
let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true")
|
||||
|
||||
let pointOfInterest = LocationHelper.currentLocation
|
||||
|
||||
|
|
|
|||
|
|
@ -350,7 +350,7 @@ struct AdminMessage {
|
|||
}
|
||||
|
||||
///
|
||||
/// Clear fixed position coordinates and then set position.fixed_position = false
|
||||
/// Clear fixed position coordinates and then set position.fixed_position = false
|
||||
var removeFixedPosition: Bool {
|
||||
get {
|
||||
if case .removeFixedPosition(let v)? = payloadVariant {return v}
|
||||
|
|
@ -547,7 +547,7 @@ struct AdminMessage {
|
|||
/// Set fixed position data on the node and then set the position.fixed_position = true
|
||||
case setFixedPosition(Position)
|
||||
///
|
||||
/// Clear fixed position coordinates and then set position.fixed_position = false
|
||||
/// Clear fixed position coordinates and then set position.fixed_position = false
|
||||
case removeFixedPosition(Bool)
|
||||
///
|
||||
/// Begins an edit transaction for config, module config, owner, and channel settings changes
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ extension MemberRole: CaseIterable {
|
|||
#endif // swift(>=4.2)
|
||||
|
||||
///
|
||||
/// Packets for the official ATAK Plugin
|
||||
/// Packets for the official ATAK Plugin
|
||||
struct TAKPacket {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
|
|
|
|||
|
|
@ -226,14 +226,14 @@ struct Config {
|
|||
///
|
||||
/// Description: Broadcasts GPS position packets as priority.
|
||||
/// Technical Details: Position Mesh packets will be prioritized higher and sent more frequently by default.
|
||||
/// When used in conjunction with power.is_power_saving = true, nodes will wake up,
|
||||
/// When used in conjunction with power.is_power_saving = true, nodes will wake up,
|
||||
/// send position, and then sleep for position.position_broadcast_secs seconds.
|
||||
case tracker // = 5
|
||||
|
||||
///
|
||||
/// Description: Broadcasts telemetry packets as priority.
|
||||
/// Technical Details: Telemetry Mesh packets will be prioritized higher and sent more frequently by default.
|
||||
/// When used in conjunction with power.is_power_saving = true, nodes will wake up,
|
||||
/// When used in conjunction with power.is_power_saving = true, nodes will wake up,
|
||||
/// send environment telemetry, and then sleep for telemetry.environment_update_interval seconds.
|
||||
case sensor // = 6
|
||||
|
||||
|
|
@ -249,12 +249,12 @@ struct Config {
|
|||
/// Technical Details: Used for nodes that "only speak when spoken to"
|
||||
/// Turns all of the routine broadcasts but allows for ad-hoc communication
|
||||
/// Still rebroadcasts, but with local only rebroadcast mode (known meshes only)
|
||||
/// Can be used for clandestine operation or to dramatically reduce airtime / power consumption
|
||||
/// Can be used for clandestine operation or to dramatically reduce airtime / power consumption
|
||||
case clientHidden // = 8
|
||||
|
||||
///
|
||||
/// Description: Broadcasts location as message to default channel regularly for to assist with device recovery.
|
||||
/// Technical Details: Used to automatically send a text message to the mesh
|
||||
/// Technical Details: Used to automatically send a text message to the mesh
|
||||
/// with the current position of the device on a frequent interval:
|
||||
/// "I'm lost! Position: lat / long"
|
||||
case lostAndFound // = 9
|
||||
|
|
|
|||
|
|
@ -1565,8 +1565,8 @@ struct MeshPacket {
|
|||
set {_uniqueStorage()._viaMqtt = newValue}
|
||||
}
|
||||
|
||||
///
|
||||
/// Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header.
|
||||
///
|
||||
/// Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header.
|
||||
/// When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled.
|
||||
var hopStart: UInt32 {
|
||||
get {return _storage._hopStart}
|
||||
|
|
@ -2606,7 +2606,7 @@ struct DeviceMetadata {
|
|||
init() {}
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// A heartbeat message is sent to the node from the client to keep the connection alive.
|
||||
/// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI.
|
||||
struct Heartbeat {
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ struct Telemetry {
|
|||
}
|
||||
|
||||
///
|
||||
/// Power Metrics
|
||||
/// Power Metrics
|
||||
var powerMetrics: PowerMetrics {
|
||||
get {
|
||||
if case .powerMetrics(let v)? = variant {return v}
|
||||
|
|
@ -392,7 +392,7 @@ struct Telemetry {
|
|||
/// Air quality metrics
|
||||
case airQualityMetrics(AirQualityMetrics)
|
||||
///
|
||||
/// Power Metrics
|
||||
/// Power Metrics
|
||||
case powerMetrics(PowerMetrics)
|
||||
|
||||
#if !swift(>=4.1)
|
||||
|
|
|
|||
|
|
@ -17,21 +17,16 @@ struct UserList: View {
|
|||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@State private var searchText = ""
|
||||
@State private var viaLora = true
|
||||
@State private var viaMqtt = true
|
||||
@State private var isOnline = false
|
||||
@State private var isFavorite = false
|
||||
@State private var distanceFilter = false
|
||||
@State private var maxDistance: Double = 800000
|
||||
@State private var hopsAway: Int = -1
|
||||
@State private var deviceRole: Int = -1
|
||||
@State var isEditingFilters = false
|
||||
|
||||
var usersQuery: Binding<String> {
|
||||
Binding {
|
||||
searchText
|
||||
} set: { newValue in
|
||||
searchText = newValue
|
||||
/// Case Insensitive Search Text Predicates
|
||||
let searchPredicates = ["userId", "hwModel", "longName", "shortName"].map { property in
|
||||
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
|
||||
}
|
||||
/// Create a compound predicate using each text search predicate as an OR
|
||||
let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates)
|
||||
users.nsPredicate = newValue.isEmpty ? nil : textSearchPredicate
|
||||
}
|
||||
}
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false),
|
||||
NSSortDescriptor(key: "userNode.favorite", ascending: false),
|
||||
|
|
@ -172,9 +167,143 @@ struct UserList: View {
|
|||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count - 1)))
|
||||
.searchable(text: usersQuery, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact")
|
||||
.sheet(isPresented: $isEditingFilters) {
|
||||
NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole)
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
searchUserList()
|
||||
}
|
||||
.onChange(of: viaLora) { _ in
|
||||
if !viaLora && !viaMqtt {
|
||||
viaMqtt = true
|
||||
}
|
||||
searchUserList()
|
||||
}
|
||||
.onChange(of: viaMqtt) { _ in
|
||||
if !viaLora && !viaMqtt {
|
||||
viaLora = true
|
||||
}
|
||||
searchUserList()
|
||||
}
|
||||
.onChange(of: deviceRole) { _ in
|
||||
searchUserList()
|
||||
}
|
||||
.onChange(of: hopsAway) { _ in
|
||||
searchUserList()
|
||||
}
|
||||
.onChange(of: isOnline) { _ in
|
||||
searchUserList()
|
||||
}
|
||||
.onChange(of: isFavorite) { _ in
|
||||
searchUserList()
|
||||
}
|
||||
.onChange(of: maxDistance) { _ in
|
||||
searchUserList()
|
||||
}
|
||||
.onChange(of: distanceFilter) { _ in
|
||||
searchUserList()
|
||||
}
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
searchUserList()
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .trailing) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isEditingFilters = !isEditingFilters
|
||||
}
|
||||
}) {
|
||||
Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.searchable(text: $searchText, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact")
|
||||
.disableAutocorrection(true)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
}
|
||||
}
|
||||
|
||||
private func searchUserList() {
|
||||
|
||||
/// Case Insensitive Search Text Predicates
|
||||
let searchPredicates = ["userId", "numString", "hwModel", "longName", "shortName"].map { property in
|
||||
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
|
||||
}
|
||||
/// Create a compound predicate using each text search preicate as an OR
|
||||
let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates)
|
||||
/// Create an array of predicates to hold our AND predicates
|
||||
var predicates: [NSPredicate] = []
|
||||
/// Mqtt
|
||||
if !(viaLora && viaMqtt) {
|
||||
if viaLora {
|
||||
let loraPredicate = NSPredicate(format: "userNode.viaMqtt == NO")
|
||||
predicates.append(loraPredicate)
|
||||
} else {
|
||||
let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES")
|
||||
predicates.append(mqttPredicate)
|
||||
}
|
||||
}
|
||||
/// Role
|
||||
if deviceRole > -1 {
|
||||
let rolePredicate = NSPredicate(format: "role == %i", Int32(deviceRole))
|
||||
predicates.append(rolePredicate)
|
||||
}
|
||||
/// Hops Away
|
||||
if hopsAway > 0 {
|
||||
let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway))
|
||||
predicates.append(hopsAwayPredicate)
|
||||
}
|
||||
|
||||
/// Online
|
||||
if isOnline {
|
||||
let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
|
||||
predicates.append(isOnlinePredicate)
|
||||
}
|
||||
/// Favorites
|
||||
if isFavorite {
|
||||
let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES")
|
||||
predicates.append(isFavoritePredicate)
|
||||
}
|
||||
/// Distance
|
||||
if distanceFilter {
|
||||
let pointOfInterest = LocationHelper.currentLocation
|
||||
|
||||
if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude {
|
||||
let D: Double = maxDistance * 1.1
|
||||
let R: Double = 6371009
|
||||
let meanLatitidue = pointOfInterest.latitude * .pi / 180
|
||||
let deltaLatitude = D / R * 180 / .pi
|
||||
let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi
|
||||
let minLatitude: Double = pointOfInterest.latitude - deltaLatitude
|
||||
let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude
|
||||
let minLongitude: Double = pointOfInterest.longitude - deltaLongitude
|
||||
let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude
|
||||
let distancePredicate = NSPredicate(format: "(SUBQUERY(userNode.positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude,minLatitude, maxLatitude)
|
||||
predicates.append(distancePredicate)
|
||||
}
|
||||
}
|
||||
|
||||
if predicates.count > 0 || !searchText.isEmpty {
|
||||
if !searchText.isEmpty {
|
||||
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)
|
||||
users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates])
|
||||
} else {
|
||||
users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates)
|
||||
}
|
||||
} else {
|
||||
users.nsPredicate = nil
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ struct NodeMapSwiftUI: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) {
|
||||
.safeAreaInset(edge: .bottom, alignment: .trailing) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import SwiftUI
|
|||
struct NodeListFilter: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
/// Filters
|
||||
var filterTitle = "Node Filters"
|
||||
@Binding var viaLora: Bool
|
||||
@Binding var viaMqtt: Bool
|
||||
@Binding var isOnline: Bool
|
||||
|
|
@ -24,7 +25,7 @@ struct NodeListFilter: View {
|
|||
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section(header: Text("Node Filters")) {
|
||||
Section(header: Text(filterTitle)) {
|
||||
Toggle(isOn: $viaLora) {
|
||||
|
||||
Label {
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ struct MeshMap: View {
|
|||
return
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) {
|
||||
.safeAreaInset(edge: .bottom, alignment: .trailing) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@
|
|||
"network.config"="Netzwerkeinstellungen";
|
||||
"nodes"="Nodes";
|
||||
"nodes %@"="Nodes (%@)";
|
||||
"nodelist.filter.distance %@"="up to %@ away";
|
||||
"no.nodes"="Keine Meshtastic Nodes gefunden";
|
||||
"not.connected"="Kein Gerät verbunden";
|
||||
"numbers.punctuation"="Ziffern und Interpunktion";
|
||||
|
|
|
|||
|
|
@ -245,6 +245,8 @@
|
|||
"network.config"="Network Config";
|
||||
"nodes"="Nodes";
|
||||
"nodes %@"="Nodes (%@)";
|
||||
"nodelist.filter.distance %@"="up to %@ away";
|
||||
"save.config %@"="Save Config for %@";
|
||||
"no.nodes"="No Meshtastic Nodes Found";
|
||||
"not.connected"="No device connected";
|
||||
"numbers.punctuation"="Numbers and Punctuation";
|
||||
|
|
|
|||
|
|
@ -219,6 +219,7 @@
|
|||
"network.config"="Configuration du réseau";
|
||||
"nodes"="Noeuds";
|
||||
"nodes %@"="Noeuds (%@)";
|
||||
"nodelist.filter.distance %@"="up to %@ away";
|
||||
"no.nodes"="Aucun noeud Meshtastic trouvé";
|
||||
"not.connected"="Aucun appareil connecté";
|
||||
"numbers.punctuation"="Nombres and Ponctuation";
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@
|
|||
"network.config"="הגדרות רשת";
|
||||
"nodes"="מכשירים";
|
||||
"nodes %@"="מכשירים (%@)";
|
||||
"nodelist.filter.distance %@"="up to %@ away";
|
||||
"no.nodes"="לא נמצאו מכשירי משטסטיק";
|
||||
"not.connected"="אין מכשיר מחובר";
|
||||
"numbers.punctuation"="מספרים וסימני פיסוק ";
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@
|
|||
"network"="Sieć";
|
||||
"network.config"="Konfiguracja sieci";
|
||||
"nodes %@"="Węzły (%@)";
|
||||
"nodelist.filter.distance %@"="up to %@ away";
|
||||
"no.nodes"="Nie znaleziono węzłów Meshtastic";
|
||||
"not.connected"="Brak podłączonych urządzeń";
|
||||
"numbers.punctuation"="Cyfry i interpunkcja";
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit dea3a82ef2accd25112b4ef1c6f8991b579740f4
|
||||
Subproject commit e6b4c590e7c489306c9c44e3ad1fcf62a3efd288
|
||||
|
|
@ -239,6 +239,7 @@
|
|||
"network.config"="网络配置";
|
||||
"nodes"="节点";
|
||||
"nodes %@"="节点 (%@)";
|
||||
"nodelist.filter.distance %@"="up to %@ away";
|
||||
"no.nodes"="未找到 Meshtastic 节点";
|
||||
"not.connected"="未连接到电台";
|
||||
"numbers.punctuation"="数字和标点符号";
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@
|
|||
"network.config"="網路設定";
|
||||
"nodes"="中繼點";
|
||||
"nodes %@"="中繼點 (%@)";
|
||||
"nodelist.filter.distance %@"="up to %@ away";
|
||||
"no.nodes"="未找到 Meshtastic 中繼點";
|
||||
"not.connected"="未連接到電台";
|
||||
"numbers.punctuation"="數字和標點符號";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue