From d0f84662db876087bf50d3aa072da23defbbbe2e Mon Sep 17 00:00:00 2001 From: Austin Payne Date: Sat, 17 Feb 2024 13:26:09 -0700 Subject: [PATCH 1/7] improvement: add MessageText views --- Meshtastic.xcodeproj/project.pbxproj | 12 ++ Meshtastic/Enums/MessageDestination.swift | 19 +++ .../Views/Messages/ChannelMessageList.swift | 141 ++---------------- .../Messages/MessageContextMenuItems.swift | 115 ++++++++++++++ Meshtastic/Views/Messages/MessageText.swift | 89 +++++++++++ .../TextMessageField/TextMessageField.swift | 25 +--- .../Views/Messages/UserMessageList.swift | 122 ++------------- 7 files changed, 257 insertions(+), 266 deletions(-) create mode 100644 Meshtastic/Enums/MessageDestination.swift create mode 100644 Meshtastic/Views/Messages/MessageContextMenuItems.swift create mode 100644 Meshtastic/Views/Messages/MessageText.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 224c2ff2..e1ffed4a 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -14,6 +14,9 @@ B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; }; C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; }; + D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */; }; + D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; }; + D93068D72B8146690066FBC8 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D62B8146690066FBC8 /* MessageText.swift */; }; D9BC22DB2B7DE8E2006A37D5 /* TileDownloadStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BC22DA2B7DE8E2006A37D5 /* TileDownloadStatus.swift */; }; D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */; }; D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */; }; @@ -235,6 +238,9 @@ B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = ""; }; + D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = ""; }; + D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; + D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; D9BC22DA2B7DE8E2006A37D5 /* TileDownloadStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileDownloadStatus.swift; sourceTree = ""; }; D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSize.swift; sourceTree = ""; }; D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = ""; }; @@ -693,6 +699,7 @@ DD1925B828CDA93900720036 /* SerialConfigEnums.swift */, DD994B68295F88B60013760A /* IntervalEnums.swift */, DD5E5239298EFA5300D21B61 /* TelemetryWeather.swift */, + D93068D42B812B700066FBC8 /* MessageDestination.swift */, ); path = Enums; sourceTree = ""; @@ -850,6 +857,8 @@ DDB8F4112A9EE5DD00230ECE /* UserList.swift */, DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */, B399E8A32B6F486400E4488E /* RetryButton.swift */, + D93068D62B8146690066FBC8 /* MessageText.swift */, + D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */, ); path = Messages; sourceTree = ""; @@ -1242,6 +1251,7 @@ DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */, DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */, 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */, + D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */, DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */, @@ -1298,6 +1308,7 @@ DD5E523A298EFA5300D21B61 /* TelemetryWeather.swift in Sources */, DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */, + D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */, @@ -1316,6 +1327,7 @@ DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */, B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */, DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */, + D93068D72B8146690066FBC8 /* MessageText.swift in Sources */, DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */, DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */, diff --git a/Meshtastic/Enums/MessageDestination.swift b/Meshtastic/Enums/MessageDestination.swift new file mode 100644 index 00000000..4b6d2b54 --- /dev/null +++ b/Meshtastic/Enums/MessageDestination.swift @@ -0,0 +1,19 @@ +/// Helper abstraction for sharing functionality between channel and direct messaging. +enum MessageDestination { + case user(UserEntity) + case channel(ChannelEntity) + + var userNum: Int64 { + switch self { + case let .user(user): return user.num + case .channel: return 0 + } + } + + var channelNum: Int32 { + switch self { + case .user: return 0 + case let .channel(channel): return channel.index + } + } +} diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 8c311d7c..81a8bdc2 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -18,15 +18,11 @@ struct ChannelMessageList: View { @ObservedObject var myInfo: MyInfoEntity @ObservedObject var channel: ChannelEntity - @State var showDeleteMessageAlert = false - @State private var deleteMessageId: Int64 = 0 @State private var replyMessageId: Int64 = 0 @AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1 var body: some View { VStack { - let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmssa", options: 0, locale: Locale.current) - let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss a") ScrollViewReader { scrollView in ScrollView { LazyVStack { @@ -55,118 +51,16 @@ struct ChannelMessageList: View { .offset(y: -5) } VStack(alignment: currentUser ? .trailing : .leading) { - let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) - let linkBlue = Color(red: 0.4627, green: 0.8392, blue: 1) /* #76d6ff */ let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) - Text(markdownText) - .tint(linkBlue) - .padding(10) - .foregroundColor(.white) - .background(currentUser ? .accentColor : Color(.gray)) - .cornerRadius(15) - .overlay( - VStack { - if #available(iOS 17.0, macOS 14.0, *) { - isDetectionSensorMessage ? Image(systemName: "sensor.fill") - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) - .foregroundStyle(Color.orange) - .symbolRenderingMode(.multicolor) - .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) - .offset(x: 20, y: -20) - : nil - } else { - isDetectionSensorMessage ? Image(systemName: "sensor.fill") - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) - .foregroundStyle(Color.orange) - .offset(x: 20, y: -20) - : nil - } - } - ) - .contextMenu { - VStack { - Text("channel")+Text(": \(message.channel)") - } - Menu("tapback") { - ForEach(Tapbacks.allCases) { tb in - Button(action: { - if bleManager.sendMessage(message: tb.emojiString, toUserNum: 0, channel: channel.index, isEmoji: true, replyID: message.messageId) { - print("Sent \(tb.emojiString) Tapback") - self.context.refresh(channel, mergeChanges: true) - } else { print("\(tb.emojiString) Tapback Failed") } + MessageText( + message: message, + tapBackDestination: .channel(channel), + isCurrentUser: currentUser + ) { + self.replyMessageId = message.messageId + self.messageFieldFocused = true + } - }) { - Text(tb.description) - let image = tb.emojiString.image() - Image(uiImage: image!) - } - } - } - Button(action: { - self.replyMessageId = message.messageId - self.messageFieldFocused = true - print("I want to reply to \(message.messageId)") - }) { - Text("reply") - Image(systemName: "arrowshape.turn.up.left.2.fill") - } - Button(action: { - UIPasteboard.general.string = message.messagePayload - }) { - Text("copy") - Image(systemName: "doc.on.doc") - } - Menu("message.details") { - VStack { - let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) - Text(" \(messageDate.formattedDate(format: dateFormatString))").foregroundColor(.gray) - } - if !currentUser { - VStack { - Text("SNR \(String(format: "%.2f", message.snr)) dB") - } - } - if currentUser && message.receivedACK { - VStack { - Text("received.ack")+Text(" \(message.receivedACK ? "✔️" : "")") - } - } else if currentUser && message.ackError == 0 { - // Empty Error - Text("waiting") - } else if currentUser && message.ackError > 0 { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) - Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - } - if currentUser { - VStack { - let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) - let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) - if ackDate >= sixMonthsAgo! { - Text("Ack Time: \(ackDate.formattedDate(format: "h:mm:ss a"))").foregroundColor(.gray) - } else { - Text("unknown.age").foregroundColor(.gray) - } - } - } - if message.ackSNR != 0 { - VStack { - Text("Ack SNR: \(String(format: "%.2f", message.ackSNR)) dB") - .foregroundColor(.gray) - } - } - } - Divider() - Button(role: .destructive, action: { - self.showDeleteMessageAlert = true - self.deleteMessageId = message.messageId - print(deleteMessageId) - }) { - Text("delete") - Image(systemName: "trash") - } - } let tapbacks = message.value(forKey: "tapbacks") as? [MessageEntity] ?? [] if tapbacks.count > 0 { VStack(alignment: .trailing) { @@ -218,7 +112,7 @@ struct ChannelMessageList: View { .font(.caption2).foregroundColor(.red) } else if isDetectionSensorMessage { let messageDate = message.timestamp - Text(" \(messageDate.formattedDate(format: dateFormatString))").font(.caption2).foregroundColor(.gray) + Text(" \(messageDate.formattedDate(format: MessageText.dateFormatString))").font(.caption2).foregroundColor(.gray) } } } @@ -236,21 +130,6 @@ struct ChannelMessageList: View { .padding([.leading, .trailing]) .frame(maxWidth: .infinity) .id(message.messageId) - .alert(isPresented: $showDeleteMessageAlert) { - Alert(title: Text("Are you sure you want to delete this message?"), message: Text("This action is permanent."), primaryButton: .destructive(Text("Delete")) { - print("OK button tapped") - if deleteMessageId > 0 { - let message = channel.allPrivateMessages.first(where: { $0.messageId == deleteMessageId }) - context.delete(message!) - do { - try context.save() - deleteMessageId = 0 - } catch { - print("Failed to delete message \(deleteMessageId)") - } - } - }, secondaryButton: .cancel()) - } .onAppear { if !message.read { message.read = true @@ -286,7 +165,7 @@ struct ChannelMessageList: View { } TextMessageField( - destination: .channel(channel.index), + destination: .channel(channel), replyMessageId: $replyMessageId, isFocused: $messageFieldFocused ) { diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift new file mode 100644 index 00000000..d98eaeec --- /dev/null +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -0,0 +1,115 @@ +import SwiftUI +import CoreData + +struct MessageContextMenuItems: View { + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + let message: MessageEntity + let tapBackDestination: MessageDestination + let isCurrentUser: Bool + @Binding var isShowingDeleteConfirmation: Bool + let onReply: () -> Void + + var body: some View { + VStack { + Text("channel") + Text(": \(message.channel)") + } + + Menu("tapback") { + ForEach(Tapbacks.allCases) { tb in + Button { + let sentMessage = bleManager.sendMessage( + message: tb.emojiString, + toUserNum: tapBackDestination.userNum, + channel: tapBackDestination.channelNum, + isEmoji: true, + replyID: message.messageId + ) + if sentMessage { + self.context.refresh(tapBackDestination.managedObject, mergeChanges: true) + } + } label: { + Text(tb.description) + Image(uiImage: tb.emojiString.image()!) + } + } + } + + Button(action: onReply) { + Text("reply") + Image(systemName: "arrowshape.turn.up.left") + } + + Button { + UIPasteboard.general.string = message.messagePayload + } label: { + Text("copy") + Image(systemName: "doc.on.doc") + } + + Menu("message.details") { + VStack { + let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) + Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))").foregroundColor(.gray) + } + if !isCurrentUser { + VStack { + Text("SNR \(String(format: "%.2f", message.snr)) dB") + } + } + if isCurrentUser && message.receivedACK { + VStack { + Text("received.ack") + Text(": \(message.receivedACK ? "✔️" : "")") + Text("received.ack.real") + Text(": \(message.realACK ? "✔️" : "")") + } + } else if isCurrentUser && message.ackError == 0 { + // Empty Error + Text("waiting") + } else if isCurrentUser && message.ackError > 0 { + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + Text("\(ackErrorVal?.display ?? "Empty Ack Error")") + .fixedSize(horizontal: false, vertical: true) + } + if isCurrentUser { + VStack { + let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) + let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) + if ackDate >= sixMonthsAgo! { + Text("Ack Time: \(ackDate.formattedDate(format: "h:mm:ss.SSSS a"))") + .foregroundColor(.gray) + } else { + Text("unknown.age") + .font(.caption2) + .foregroundColor(.gray) + } + } + } + if message.ackSNR != 0 { + VStack { + Text("Ack SNR: \(String(format: "%.2f", message.ackSNR)) dB") + .font(.caption2) + .foregroundColor(.gray) + } + } + } + + Divider() + + Button(role: .destructive) { + isShowingDeleteConfirmation = true + } label: { + Text("delete") + Image(systemName: "trash") + } + } +} + +private extension MessageDestination { + var managedObject: NSManagedObject { + switch self { + case let .user(user): return user + case let .channel(channel): return channel + } + } +} diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift new file mode 100644 index 00000000..67500bc2 --- /dev/null +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -0,0 +1,89 @@ +import SwiftUI + +struct MessageText: View { + static let linkBlue = Color(red: 0.4627, green: 0.8392, blue: 1) /* #76d6ff */ + static let localeDateFormat = DateFormatter.dateFormat( + fromTemplate: "yyMMddjmmssa", + options: 0, + locale: Locale.current + ) + static let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss:a") + + @Environment(\.managedObjectContext) var context + + let message: MessageEntity + let tapBackDestination: MessageDestination + let isCurrentUser: Bool + let onReply: () -> Void + + @State private var isShowingDeleteConfirmation = false + + var body: some View { + let markdownText = LocalizedStringKey(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) + return Text(markdownText) + .tint(Self.linkBlue) + .padding(10) + .foregroundColor(.white) + .background(isCurrentUser ? .accentColor : Color(.gray)) + .cornerRadius(15) + .overlay { + let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) + if tapBackDestination.overlaySensorMessage { + VStack { + if #available(iOS 17.0, macOS 14.0, *) { + isDetectionSensorMessage ? Image(systemName: "sensor.fill") + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) + .foregroundStyle(Color.orange) + .symbolRenderingMode(.multicolor) + .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) + .offset(x: 20, y: -20) + : nil + } else { + isDetectionSensorMessage ? Image(systemName: "sensor.fill") + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) + .foregroundStyle(Color.orange) + .offset(x: 20, y: -20) + : nil + } + } + } else { + EmptyView() + } + } + .contextMenu { + MessageContextMenuItems( + message: message, + tapBackDestination: tapBackDestination, + isCurrentUser: isCurrentUser, + isShowingDeleteConfirmation: $isShowingDeleteConfirmation, + onReply: onReply + ) + } + .confirmationDialog( + "Are you sure you want to delete this message?", + isPresented: $isShowingDeleteConfirmation, + titleVisibility: .visible + ) { + Button("Delete Message", role: .destructive) { + context.delete(message) + do { + try context.save() + } catch { + print("Failed to delete message \(message.messageId)") + } + } + Button("Cancel", role: .cancel) {} + } + } +} + +private extension MessageDestination { + var overlaySensorMessage: Bool { + switch self { + case .user: return false + case .channel: return true + } + } +} diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift index 67175642..e5cd01d3 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -4,15 +4,10 @@ struct TextMessageField: View { static let maxbytes = 228 @EnvironmentObject var bleManager: BLEManager - let destination: Destination + let destination: MessageDestination @Binding var replyMessageId: Int64 @FocusState.Binding var isFocused: Bool let onSubmit: () -> Void - - enum Destination { - case user(Int64) - case channel(Int32) - } @State private var typingMessage: String = "" @State private var totalBytes = 0 @@ -125,7 +120,7 @@ struct TextMessageField: View { } } -private extension TextMessageField.Destination { +private extension MessageDestination { var positionShareMessage: String { switch self { case .user: return "has shared their position and requested a response with your position" @@ -133,23 +128,9 @@ private extension TextMessageField.Destination { } } - var userNum: Int64 { - switch self { - case let .user(num): return num - case .channel: return 0 - } - } - - var channelNum: Int32 { - switch self { - case .user: return 0 - case let .channel(num): return num - } - } - var positionDestNum: Int64 { switch self { - case let .user(num): return num + case let .user(user): return user.num case .channel: return Int64(BLEManager.emptyNodeNum) } } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index b595576f..64bf9aeb 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -17,14 +17,10 @@ struct UserMessageList: View { @FocusState var messageFieldFocused: Bool // View State Items @ObservedObject var user: UserEntity - @State var showDeleteMessageAlert = false - @State private var deleteMessageId: Int64 = 0 @State private var replyMessageId: Int64 = 0 var body: some View { VStack { - let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmss", options: 0, locale: Locale.current) - let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss:a") ScrollViewReader { scrollView in ScrollView { LazyVStack { @@ -50,100 +46,14 @@ struct UserMessageList: View { HStack(alignment: .top) { if currentUser { Spacer(minLength: 50) } VStack(alignment: currentUser ? .trailing : .leading) { - let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) - - let linkBlue = Color(red: 0.4627, green: 0.8392, blue: 1) /* #76d6ff */ - Text(markdownText) - .tint(linkBlue) - .padding(10) - .foregroundColor(.white) - .background(currentUser ? .accentColor : Color(.gray)) - .cornerRadius(15) - .contextMenu { - VStack { - Text("channel")+Text(": \(message.channel)") - } - Menu("tapback") { - ForEach(Tapbacks.allCases) { tb in - Button(action: { - if bleManager.sendMessage(message: tb.emojiString, toUserNum: user.num, channel: 0, isEmoji: true, replyID: message.messageId) { - print("Sent \(tb.emojiString) Tapback") - self.context.refresh(user, mergeChanges: true) - } else { print("\(tb.emojiString) Tapback Failed") } - - }) { - Text(tb.description) - let image = tb.emojiString.image() - Image(uiImage: image!) - } - } - } - Button(action: { - self.replyMessageId = message.messageId - self.messageFieldFocused = true - print("I want to reply to \(message.messageId)") - }) { - Text("reply") - Image(systemName: "arrowshape.turn.up.left.2.fill") - } - Button(action: { - UIPasteboard.general.string = message.messagePayload - }) { - Text("copy") - Image(systemName: "doc.on.doc") - } - Menu("message.details") { - VStack { - - let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) - Text("\(messageDate.formattedDate(format: dateFormatString))").foregroundColor(.gray) - } - if !currentUser { - VStack { - Text("SNR \(String(format: "%.2f", message.snr)) dB") - } - } - if currentUser && message.receivedACK { - VStack { - Text("received.ack")+Text(" \(message.receivedACK ? "✔️" : "")") - Text("received.ack.real")+Text(" \(message.realACK ? "✔️" : "")") - } - } else if currentUser && message.ackError == 0 { - // Empty Error - Text("waiting") - } else if currentUser && message.ackError > 0 { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) - Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - } - if currentUser { - VStack { - let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) - let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) - if ackDate >= sixMonthsAgo! { - Text("Ack Time: \(ackDate.formattedDate(format: "h:mm:ss.SSSS a"))").foregroundColor(.gray) - } else { - Text("unknown.age").font(.caption2).foregroundColor(.gray) - } - } - } - if message.ackSNR != 0 { - VStack { - Text("Ack SNR: \(String(format: "%.2f", message.ackSNR)) dB") - .font(.caption2) - .foregroundColor(.gray) - } - } - } - Divider() - Button(role: .destructive, action: { - self.showDeleteMessageAlert = true - self.deleteMessageId = message.messageId - print(deleteMessageId) - }) { - Text("delete") - Image(systemName: "trash") - } - } + MessageText( + message: message, + tapBackDestination: .user(user), + isCurrentUser: currentUser + ) { + self.replyMessageId = message.messageId + self.messageFieldFocused = true + } let tapbacks = message.value(forKey: "tapbacks") as? [MessageEntity] ?? [] if tapbacks.count > 0 { @@ -214,20 +124,6 @@ struct UserMessageList: View { .padding([.leading, .trailing]) .frame(maxWidth: .infinity) .id(message.messageId) - .alert(isPresented: $showDeleteMessageAlert) { - Alert(title: Text("Are you sure you want to delete this message?"), message: Text("This action is permanent."), primaryButton: .destructive(Text("Delete")) { - if deleteMessageId > 0 { - let message = user.messageList.first(where: { $0.messageId == deleteMessageId }) - context.delete(message!) - do { - try context.save() - deleteMessageId = 0 - } catch { - print("Failed to delete message \(deleteMessageId)") - } - } - }, secondaryButton: .cancel()) - } .onAppear { if !message.read { message.read = true @@ -264,7 +160,7 @@ struct UserMessageList: View { } TextMessageField( - destination: .user(user.num), + destination: .user(user), replyMessageId: $replyMessageId, isFocused: $messageFieldFocused ) { From bb0acba742c9c1aaa5619597b86893a21e560a2e Mon Sep 17 00:00:00 2001 From: Austin Payne Date: Sat, 17 Feb 2024 13:26:49 -0700 Subject: [PATCH 2/7] improvement: dedupe canRetry definition --- Meshtastic/Extensions/CoreData/MessageEntityExtension.swift | 4 ++++ Meshtastic/Views/Messages/ChannelMessageList.swift | 2 +- Meshtastic/Views/Messages/UserMessageList.swift | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index 5f3d76ef..81e106b8 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -17,4 +17,8 @@ extension MessageEntity { let time = messageTimestamp <= 0 ? receivedTimestamp : messageTimestamp return Date(timeIntervalSince1970: TimeInterval(time)) } + + var canRetry: Bool { + return ackError == 9 || ackError == 5 || ackError == 3 + } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 81a8bdc2..4ec56ec9 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -119,7 +119,7 @@ struct ChannelMessageList: View { .padding(.bottom) .id(channel.allPrivateMessages.firstIndex(of: message)) - if currentUser && (message.ackError == 9 || message.ackError == 5 || message.ackError == 3) { + if currentUser && message.canRetry { RetryButton(message: message) } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 64bf9aeb..1eb2ddc8 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -113,7 +113,7 @@ struct UserMessageList: View { .padding(.bottom) .id(user.messageList.firstIndex(of: message)) - if currentUser && (message.ackError == 9 || message.ackError == 5 || message.ackError == 3) || (message.receivedACK && !message.realACK) { + if currentUser && message.canRetry || (message.receivedACK && !message.realACK) { RetryButton(message: message) } From 8b751f462aff35164a19f0d643568dc4a6252931 Mon Sep 17 00:00:00 2001 From: Austin Payne Date: Sat, 17 Feb 2024 13:51:17 -0700 Subject: [PATCH 3/7] improvement: add TapbackResponses view --- Meshtastic.xcodeproj/project.pbxproj | 4 ++ .../Views/Messages/ChannelMessageList.swift | 40 ++------------- .../Views/Messages/TapbackResponses.swift | 49 +++++++++++++++++++ .../Views/Messages/UserMessageList.swift | 40 ++------------- 4 files changed, 61 insertions(+), 72 deletions(-) create mode 100644 Meshtastic/Views/Messages/TapbackResponses.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e1ffed4a..c6bef2b3 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */; }; D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; }; D93068D72B8146690066FBC8 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D62B8146690066FBC8 /* MessageText.swift */; }; + D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D82B81509C0066FBC8 /* TapbackResponses.swift */; }; D9BC22DB2B7DE8E2006A37D5 /* TileDownloadStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BC22DA2B7DE8E2006A37D5 /* TileDownloadStatus.swift */; }; D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */; }; D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */; }; @@ -241,6 +242,7 @@ D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = ""; }; D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; + D93068D82B81509C0066FBC8 /* TapbackResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapbackResponses.swift; sourceTree = ""; }; D9BC22DA2B7DE8E2006A37D5 /* TileDownloadStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileDownloadStatus.swift; sourceTree = ""; }; D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSize.swift; sourceTree = ""; }; D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = ""; }; @@ -859,6 +861,7 @@ B399E8A32B6F486400E4488E /* RetryButton.swift */, D93068D62B8146690066FBC8 /* MessageText.swift */, D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */, + D93068D82B81509C0066FBC8 /* TapbackResponses.swift */, ); path = Messages; sourceTree = ""; @@ -1291,6 +1294,7 @@ DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */, DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */, + D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */, DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */, DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */, DD5E5212298EE33B00D21B61 /* apponly.pb.swift in Sources */, diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 4ec56ec9..406623a0 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -61,42 +61,10 @@ struct ChannelMessageList: View { self.messageFieldFocused = true } - 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) - } - .onAppear { - if !tapback.read { - tapback.read = true - do { - try context.save() - print("📖 Read message \(message.messageId) ") - appState.unreadChannelMessages = myInfo.unreadMessages - UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages - context.refresh(myInfo, mergeChanges: true) - } catch { - print("Failed to read tapback \(tapback.messageId)") - } - } - } - } - } - .padding(10) - .overlay( - RoundedRectangle(cornerRadius: 18) - .stroke(Color.gray, lineWidth: 1) - ) - } + TapbackResponses(message: message) { + appState.unreadChannelMessages = myInfo.unreadMessages + UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages + context.refresh(myInfo, mergeChanges: true) } HStack { diff --git a/Meshtastic/Views/Messages/TapbackResponses.swift b/Meshtastic/Views/Messages/TapbackResponses.swift new file mode 100644 index 00000000..a7685697 --- /dev/null +++ b/Meshtastic/Views/Messages/TapbackResponses.swift @@ -0,0 +1,49 @@ +import SwiftUI + +struct TapbackResponses: View { + @Environment(\.managedObjectContext) var context + + let message: MessageEntity + let onRead: () -> Void + + @ViewBuilder + var body: some View { + let tapbacks = message.value(forKey: "tapbacks") as? [MessageEntity] ?? [] + if !tapbacks.isEmpty { + 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) + } + .onAppear { + guard !tapback.read else { + return + } + + tapback.read = true + do { + try context.save() + print("📖 Read tapback \(tapback.messageId) ") + onRead() + } catch { + print("Failed to read tapback \(tapback.messageId)") + } + } + } + } + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.gray, lineWidth: 1) + ) + } + } + } +} diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 1eb2ddc8..9f777b4a 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -55,43 +55,11 @@ struct UserMessageList: View { self.messageFieldFocused = true } - 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) - } - .onAppear { - if !tapback.read { - tapback.read = true - do { - try context.save() - print("📖 Read tapback \(tapback.messageId) ") - appState.unreadDirectMessages = user.unreadMessages - UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages - - } catch { - print("Failed to read tapback \(tapback.messageId)") - } - } - } - } - } - .padding(10) - .overlay( - RoundedRectangle(cornerRadius: 18) - .stroke(Color.gray, lineWidth: 1) - ) - } + TapbackResponses(message: message) { + appState.unreadDirectMessages = user.unreadMessages + UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages } + HStack { let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) if currentUser && message.receivedACK { From 9efdd568d8adc41cb8e802b9884a339c40802f55 Mon Sep 17 00:00:00 2001 From: Austin Payne Date: Sat, 17 Feb 2024 13:59:42 -0700 Subject: [PATCH 4/7] fix: don't push ack text left Aligns RetryButton to look more like iMessages retry button --- .../Views/Messages/ChannelMessageList.swift | 25 +++++++++++-------- .../Views/Messages/UserMessageList.swift | 24 ++++++++++-------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 406623a0..d645b613 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -52,13 +52,20 @@ struct ChannelMessageList: View { } VStack(alignment: currentUser ? .trailing : .leading) { let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) - MessageText( - message: message, - tapBackDestination: .channel(channel), - isCurrentUser: currentUser - ) { - self.replyMessageId = message.messageId - self.messageFieldFocused = true + + HStack { + MessageText( + message: message, + tapBackDestination: .channel(channel), + isCurrentUser: currentUser + ) { + self.replyMessageId = message.messageId + self.messageFieldFocused = true + } + + if currentUser && message.canRetry { + RetryButton(message: message) + } } TapbackResponses(message: message) { @@ -87,10 +94,6 @@ struct ChannelMessageList: View { .padding(.bottom) .id(channel.allPrivateMessages.firstIndex(of: message)) - if currentUser && message.canRetry { - RetryButton(message: message) - } - if !currentUser { Spacer(minLength: 50) } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 9f777b4a..7816167d 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -46,13 +46,19 @@ struct UserMessageList: View { HStack(alignment: .top) { if currentUser { Spacer(minLength: 50) } VStack(alignment: currentUser ? .trailing : .leading) { - MessageText( - message: message, - tapBackDestination: .user(user), - isCurrentUser: currentUser - ) { - self.replyMessageId = message.messageId - self.messageFieldFocused = true + HStack { + MessageText( + message: message, + tapBackDestination: .user(user), + isCurrentUser: currentUser + ) { + self.replyMessageId = message.messageId + self.messageFieldFocused = true + } + + if currentUser && message.canRetry || (message.receivedACK && !message.realACK) { + RetryButton(message: message) + } } TapbackResponses(message: message) { @@ -81,10 +87,6 @@ struct UserMessageList: View { .padding(.bottom) .id(user.messageList.firstIndex(of: message)) - if currentUser && message.canRetry || (message.receivedACK && !message.realACK) { - RetryButton(message: message) - } - if !currentUser { Spacer(minLength: 50) } From bfd71ca2638312447240b87f0c9a06ba7b44c723 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 17 Feb 2024 14:46:09 -0800 Subject: [PATCH 5/7] array of restricted channels for messages --- Meshtastic/Tips/ChannelTips.swift | 17 ++++++ Meshtastic/Views/Messages/ChannelList.swift | 7 +-- Meshtastic/Views/Settings/AppSettings.swift | 58 +++++++++---------- Meshtastic/Views/Settings/Channels.swift | 32 ++++++---- .../Config/Module/StoreForwardConfig.swift | 2 +- Meshtastic/Views/Settings/Settings.swift | 38 ++++++------ en.lproj/Localizable.strings | 10 ++-- 7 files changed, 97 insertions(+), 67 deletions(-) diff --git a/Meshtastic/Tips/ChannelTips.swift b/Meshtastic/Tips/ChannelTips.swift index cdd5d9c7..871af684 100644 --- a/Meshtastic/Tips/ChannelTips.swift +++ b/Meshtastic/Tips/ChannelTips.swift @@ -25,3 +25,20 @@ Image(systemName: "qrcode") } } + +@available(iOS 17.0, macOS 14.0, *) +struct CreateChannelsTip: Tip { + + var id: String { + return "tip.channels.create" + } + var title: Text { + Text("tip.channels.create.title") + } + var message: Text? { + Text("tip.channels.create.message") + } + var image: Image? { + Image(systemName: "fibrechannel") + } +} diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index e6c8f99d..7ed6fb04 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -21,6 +21,8 @@ struct ChannelList: View { @State private var isPresentingTraceRouteSentAlert = false + var restrictedChannels = ["admin", "gpio", "mqtt", "serial"] + var body: some View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY") @@ -29,7 +31,7 @@ struct ChannelList: View { // Display Contacts for the rest of the non admin channels if node != nil && node!.myInfo != nil && node!.myInfo!.channels != nil { List(node!.myInfo!.channels!.array as! [ChannelEntity], id: \.self, selection: $channelSelection) { (channel: ChannelEntity) in - if channel.name?.lowercased() ?? "" != "admin" && channel.name?.lowercased() ?? "" != "gpio" && channel.name?.lowercased() ?? "" != "serial" { + if !restrictedChannels.contains(channel.name?.lowercased() ?? "") { NavigationLink(destination: ChannelMessageList(myInfo: node!.myInfo!, channel: channel)) { @@ -85,9 +87,6 @@ struct ChannelList: View { .foregroundColor(.secondary) } } -// Image(systemName: "chevron.forward") -// .font(.caption) -// .foregroundColor(.secondary) } if channel.allPrivateMessages.count > 0 { diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 579aeda5..885379e5 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -19,7 +19,28 @@ struct AppSettings: View { var body: some View { VStack { Form { - Section(header: Text("options")) { + Section(header: Text("Location Settings")) { + Toggle(isOn: $provideLocation) { + Label("appsettings.provide.location", systemImage: "location.circle.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + if provideLocation { + Toggle(isOn: $enableSmartPosition) { + Label("appsettings.smartposition", systemImage: "brain.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + VStack { + Picker("update.interval", selection: $provideLocationInterval) { + ForEach(LocationUpdateInterval.allCases) { lu in + Text(lu.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("phone.gps.interval.description") + .font(.caption2) + .foregroundColor(.gray) + } + } Toggle(isOn: $useLegacyMap) { Label("map.use.legacy", systemImage: "map") } @@ -63,35 +84,6 @@ struct AppSettings: View { } } } - Section(header: Text("Location Settings")) { - Toggle(isOn: $provideLocation) { - Label("appsettings.provide.location", systemImage: "location.circle.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if provideLocation { - Toggle(isOn: $enableSmartPosition) { - Label("appsettings.smartposition", systemImage: "brain.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onChange(of: (enableSmartPosition)) { newEnableSmartPosition in - UserDefaults.enableSmartPosition = newEnableSmartPosition - } - VStack { - Picker("update.interval", selection: $provideLocationInterval) { - ForEach(LocationUpdateInterval.allCases) { lu in - Text(lu.description) - } - } - .pickerStyle(DefaultPickerStyle()) - .onChange(of: (provideLocationInterval)) { newProvideLocationInterval in - UserDefaults.provideLocationInterval = newProvideLocationInterval - } - Text("phone.gps.interval.description") - .font(.caption2) - .foregroundColor(.gray) - } - } - } Section(header: Text("App Data")) { Button { isPresentingCoreDataResetConfirm = true @@ -158,6 +150,12 @@ struct AppSettings: View { self.bleManager.sendWantConfig() } } + .onChange(of: enableSmartPosition) { newEnableSmartPosition in + UserDefaults.enableSmartPosition = newEnableSmartPosition + } + .onChange(of: (provideLocationInterval)) { newProvideLocationInterval in + UserDefaults.provideLocationInterval = newProvideLocationInterval + } .onChange(of: useLegacyMap) { newMapUseLegacy in UserDefaults.mapUseLegacy = newMapUseLegacy } diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 4878fa2c..33fca131 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -7,6 +7,9 @@ import SwiftUI import CoreData +#if canImport(TipKit) +import TipKit +#endif func generateChannelKey(size: Int) -> String { var keyData = Data(count: size) @@ -40,7 +43,11 @@ struct Channels: View { var body: some View { VStack { + List { + if #available(iOS 17.0, macOS 14.0, *) { + TipView(CreateChannelsTip(), arrowEdge: .bottom) + } if node != nil && node?.myInfo != nil { ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in Button(action: { @@ -91,7 +98,9 @@ struct Channels: View { if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil { Button { - let key = generateChannelKey(size: 32) + channelKeySize = 16 + let key = generateChannelKey(size: channelKeySize) + channelName = "" channelIndex = Int32(node!.myInfo!.channels!.array.count) channelRole = 2 @@ -201,18 +210,21 @@ struct Channels: View { .disabled(channelKeySize <= 0) } HStack { - Text("Role") - Spacer() - Picker("Channel Role", selection: $channelRole) { - if channelRole == 1 { + if channelRole == 1 { + Picker("Channel Role", selection: $channelRole) { Text("Primary").tag(1) - } else { - Text("Disabled").tag(0) - Text("Secondary").tag(2) } + .pickerStyle(.automatic) + .disabled(true) + } else { + Text("Channel Role") + Spacer() + Picker("Channel Role", selection: $channelRole) { + Text("Disabled").tag(0) + Text("Secondary").tag(2) + } + .pickerStyle(.segmented) } - .pickerStyle(.segmented) - .disabled(channelRole == 1) } Toggle("Uplink Enabled", isOn: $uplink) .toggleStyle(SwitchToggleStyle(tint: .accentColor)) diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index 980b02a2..2f9cc65c 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -91,7 +91,7 @@ struct StoreForwardConfig: View { } if isRouter { - Section(header: Text("options")) { + Section(header: Text("Router Options")) { Toggle(isOn: $heartbeat) { Label("storeforward.heartbeat", systemImage: "waveform.path.ecg") } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 99dfe757..d2f860b0 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -117,27 +117,10 @@ struct Settings: View { } } } else { - Text("Configuring Node \(node?.user?.longName ?? "unknown".localized)") + Text("Connected Node \(node?.user?.longName ?? "unknown".localized)") } } Section("radio.configuration") { - NavigationLink { - ShareChannels(node: nodes.first(where: { $0.num == preferredNodeNum })) - } label: { - Image(systemName: "qrcode") - .symbolRenderingMode(.hierarchical) - Text("share.channels") - } - .tag(SettingsSidebar.shareChannels) - .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) - NavigationLink { - UserConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "person.crop.rectangle.fill") - .symbolRenderingMode(.hierarchical) - Text("user") - } - .tag(SettingsSidebar.userConfig) NavigationLink { LoRaConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { @@ -155,6 +138,25 @@ struct Settings: View { } .tag(SettingsSidebar.channelConfig) .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) + NavigationLink { + ShareChannels(node: nodes.first(where: { $0.num == preferredNodeNum })) + } label: { + Image(systemName: "qrcode") + .symbolRenderingMode(.hierarchical) + Text("share.channels") + } + .tag(SettingsSidebar.shareChannels) + .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) + } + Section("device.configuration") { + NavigationLink { + UserConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "person.crop.rectangle.fill") + .symbolRenderingMode(.hierarchical) + Text("user") + } + .tag(SettingsSidebar.userConfig) NavigationLink { BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 707334a0..250af743 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -81,7 +81,7 @@ "device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile devices."; "direct.messages"="Direct Messages"; "dismiss.keyboard"="Dismiss"; -"display"="Display (Device Screen)"; +"display"="Display"; "display.config"="Display Config"; "distance"="Distance"; "disconnect"="Disconnect"; @@ -270,7 +270,7 @@ "serial.mode.txtmsg"="Text Message"; "serial.mode.nmea"="NMEA Positions"; "settings"="Settings"; -"share.channels"="Share Channels QR Code"; +"share.channels"="Share QR Code"; "share.position"="Share Position"; "subscribed"="Subscribed to mesh"; "select.contact"="Select a Contact"; @@ -297,9 +297,11 @@ "timeout"="Timeout"; "timestamp"="Timestamp"; "tip.bluetooth.connect.title"="Connected Radio"; -"tip.bluetooth.connect.message"="Shows information for the Lora radio currently connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; +"tip.bluetooth.connect.message"="Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; +"tip.channels.create.title"="Manage Channels"; +"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="Sharing Meshtastic Channels"; -"tip.channels.share.message"="In a Meshtastic LoRa Mesh there are up to 8 channels. The first one is the Primary channel where most activity happens and is required. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. It talks on its primary and your secondary channel. A channel with the name 'admin' controls nodes remotely. Other channels are for private groups, each with its own key."; +"tip.channels.share.message"="A Meshtastic QR code contains the LoRa config and channel values needed to communicate. Most mesh activity takes place on the required Primary channel. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. Other channels are for private groups, each with its own key."; "tip.messages.title"="Messages"; "tip.messages.message"="You can send and receive channel (group chats) and direct messages. From any message you can long press to see available actions like copy, reply, tapback and delete as well as delivery details."; "twitter"="Twitter"; From 03e39454c42ca164ec0a371c901231448e556590 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 17 Feb 2024 14:51:31 -0800 Subject: [PATCH 6/7] Add missing localized keys --- de.lproj/Localizable.strings | 2 ++ he.lproj/Localizable.strings | 3 ++- pl.lproj/Localizable.strings | 2 ++ zh-Hans.lproj/Localizable.strings | 2 ++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 548a9b04..02c6954c 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -291,6 +291,8 @@ "timestamp"="Timestamp"; "tip.bluetooth.connect.title"="Connected LoRa Radio"; "tip.bluetooth.connect.message"="Shows information for the Lora radio currently connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; +"tip.channels.create.title"="Manage Channels"; +"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="Sharing Meshtastic Channels"; "tip.channels.share.message"="In a Meshtastic LoRa Mesh there are up to 8 channels. The first one is the Primary channel where most activity happens and is required. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. It talks on its primary and your secondary channel. A channel with the name 'admin' controls nodes remotely. Other channels are for private groups, each with its own key."; "tip.messages.title"="Messages"; diff --git a/he.lproj/Localizable.strings b/he.lproj/Localizable.strings index f84e47a6..041b5794 100644 --- a/he.lproj/Localizable.strings +++ b/he.lproj/Localizable.strings @@ -297,8 +297,9 @@ "timeout"="זמן קצוב"; "timestamp"="שעה/תאריך"; "tip.bluetooth.connect.title"="מכשיר מחובר"; -מראה מידע אודות מכשיר המשטסטיק המחובר כעת לבלוטוס. ניתן לגרור שמאלה להתנתקות או לחיצה ארוכה לראות סטטיסטיקה או להתחיל פעילות. "tip.bluetooth.connect.message"="מראה מידע אודות מכשיר המשטסטיק המחובר כעת לבלוטוס. ניתן לגרור שמאלה להתנתקות או לחיצה ארוכה לראות סטטיסטיקה או להתחיל פעילות."; +"tip.channels.create.title"="Manage Channels"; +"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="משתף ערוצי משטסטיק"; "tip.channels.share.message"="במשטסטיק יש עד 8 ערוצים. הראשון הינו הראשי והינו היכן שרוב הפעילות מתבצעת והכרחי. אם לא תשתף את הערוץ הראשי שלך הערוץ הראשון שלך נהיה הערוץ הראשי ברשת השניה. הוא מדבר בערוץ הראשי שלו במשני שלך. ערוץ בעל השם 'admin' הינו לשליטה מרחוק. ערוצים נוספים הינם לקבוצות פרטיות, כל אחת עם מפתח הצפנה משלה."; "tip.messages.title"="הודעות"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index bea348c0..02aae0a4 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -292,6 +292,8 @@ "timestamp"="Znacznik czasu"; "tip.bluetooth.connect.title"="Connected LoRa Radio"; "tip.bluetooth.connect.message"="Shows information for the Lora radio currently connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; +"tip.channels.create.title"="Manage Channels"; +"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="Sharing Meshtastic Channels"; "tip.channels.share.message"="In a Meshtastic LoRa Mesh there are up to 8 channels. The first one is the Primary channel where most activity happens and is required. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. It talks on its primary and your secondary channel. A channel with the name 'admin' controls nodes remotely. Other channels are for private groups, each with its own key."; "tip.messages.title"="Messages"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 178833e2..69ddc953 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -291,6 +291,8 @@ "timestamp"="时间戳"; "tip.bluetooth.connect.title"="连接到 LoRa 电台"; "tip.bluetooth.connect.message"="显示当前通过蓝牙连接的 Lora 电台的信息。您可以向左滑动断开电台,长按查看统计信息或开始实时活动。"; +"tip.channels.create.title"="Manage Channels"; +"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="共享 Meshtastic 频道"; "tip.channels.share.message"="在 Meshtastic 网络中最多有 8 个频道。第一个频道是主频道,大多数活动都发生在这里,也是必需的。如果您不共享主频道,您的第一个共享频道就会成为其他网络的主频道。它会在其主频道和您的辅助频道上对话。名称为 admin 的频道可远程控制节点。其他频道用于私人群组,每个群组都有自己的密钥。"; "tip.messages.title"="消息"; From d9d8415109ad1eb0d954d3cddc8ec22ec5c54d5c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 17 Feb 2024 15:08:01 -0800 Subject: [PATCH 7/7] Contact tip cleanup --- Meshtastic/Tips/MessagesTips.swift | 2 +- Meshtastic/Views/Messages/UserList.swift | 3 --- Meshtastic/Views/Nodes/Helpers/NodeListItem.swift | 7 +++---- en.lproj/Localizable.strings | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Tips/MessagesTips.swift b/Meshtastic/Tips/MessagesTips.swift index b2bdf295..c50af1b6 100644 --- a/Meshtastic/Tips/MessagesTips.swift +++ b/Meshtastic/Tips/MessagesTips.swift @@ -38,7 +38,7 @@ struct ContactsTip: Tip { } var message: Text? { //Text("tip.messages.contacts.message") - Text("Each node shows as an available contact. Nodes with recent messages and favorites show up at the top of the list. Select a node to send or view messages. Long press to favorite or mute the node, send a trace route or delete the conversation.") + Text("Each node is an available contact. Contacts with recent messages or marked as favorites show up at the top of the list. Select a contact to send or view messages. Long press to favorite or mute the contact or delete the conversation.") } var image: Image? { Image(systemName: "person.circle") diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index f102b6bd..e4fa4b55 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -87,9 +87,6 @@ struct UserList: View { .foregroundColor(.secondary) } } - // Image(systemName: "chevron.forward") - // .font(.caption) - // .foregroundColor(.secondary) } if user.messageList.count > 0 { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index b9e7bf9d..83008279 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -124,7 +124,7 @@ struct NodeListItem: View { .frame(width: 30) Text("Channel: \(node.channel)") .foregroundColor(.gray) - .font(.caption) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) } if node.viaMqtt && connectedNode != node.num { Image(systemName: "network") @@ -133,7 +133,7 @@ struct NodeListItem: View { .frame(width: 30) Text("Via MQTT") .foregroundColor(.gray) - .font(.caption) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) } } if node.hasPositions || node.hasEnvironmentMetrics || node.hasDetectionSensorMetrics || node.hasTraceRoutes { @@ -144,7 +144,7 @@ struct NodeListItem: View { .frame(width: 30) Text("Logs:") .foregroundColor(.gray) - .font(.callout) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) if node.hasDeviceMetrics { Image(systemName: "flipphone") .symbolRenderingMode(.hierarchical) @@ -183,7 +183,6 @@ struct NodeListItem: View { LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true) } - .padding(.top) } } .frame(maxWidth: .infinity, alignment: .leading) diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 250af743..79a0fdd4 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -299,7 +299,7 @@ "tip.bluetooth.connect.title"="Connected Radio"; "tip.bluetooth.connect.message"="Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; "tip.channels.create.title"="Manage Channels"; -"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)"; +"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/tips/)"; "tip.channels.share.title"="Sharing Meshtastic Channels"; "tip.channels.share.message"="A Meshtastic QR code contains the LoRa config and channel values needed to communicate. Most mesh activity takes place on the required Primary channel. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. Other channels are for private groups, each with its own key."; "tip.messages.title"="Messages";