diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index e0d8b147..851cba29 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -15,8 +15,8 @@ DD47E3D226F1210600029299 /* HelperFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D126F1210600029299 /* HelperFunctions.swift */; }; DD47E3D626F17ED900029299 /* CircleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D526F17ED900029299 /* CircleText.swift */; }; DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D826F3093800029299 /* MessageBubble.swift */; }; - DD47E3DB26F3901B00029299 /* MessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3DA26F3901A00029299 /* MessageList.swift */; }; - DD47E3DD26F390A000029299 /* MessageDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3DC26F390A000029299 /* MessageDetail.swift */; }; + DD47E3DB26F3901B00029299 /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3DA26F3901A00029299 /* Channels.swift */; }; + DD47E3DD26F390A000029299 /* Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3DC26F390A000029299 /* Messages.swift */; }; DD47E3DF26F39D9F00029299 /* MyInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3DE26F39D9F00029299 /* MyInfoModel.swift */; }; DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD836AE626F6B38600ABCC23 /* Connect.swift */; }; DD836AED26F858F900ABCC23 /* MeshData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD836AEC26F858F900ABCC23 /* MeshData.swift */; }; @@ -72,8 +72,8 @@ DD47E3D126F1210600029299 /* HelperFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperFunctions.swift; sourceTree = ""; }; DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = ""; }; DD47E3D826F3093800029299 /* MessageBubble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubble.swift; sourceTree = ""; }; - DD47E3DA26F3901A00029299 /* MessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageList.swift; sourceTree = ""; }; - DD47E3DC26F390A000029299 /* MessageDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDetail.swift; sourceTree = ""; }; + DD47E3DA26F3901A00029299 /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = ""; }; + DD47E3DC26F390A000029299 /* Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Messages.swift; sourceTree = ""; }; DD47E3DE26F39D9F00029299 /* MyInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoModel.swift; sourceTree = ""; }; DD836AE626F6B38600ABCC23 /* Connect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connect.swift; sourceTree = ""; }; DD836AEC26F858F900ABCC23 /* MeshData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshData.swift; sourceTree = ""; }; @@ -278,8 +278,8 @@ DDC2E18B26CE25A70042C5E4 /* Messages */ = { isa = PBXGroup; children = ( - DD47E3DA26F3901A00029299 /* MessageList.swift */, - DD47E3DC26F390A000029299 /* MessageDetail.swift */, + DD47E3DA26F3901A00029299 /* Channels.swift */, + DD47E3DC26F390A000029299 /* Messages.swift */, ); path = Messages; sourceTree = ""; @@ -450,7 +450,7 @@ DDAF8C5F26ED09B50058C060 /* radioconfig.pb.swift in Sources */, DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, - DD47E3DB26F3901B00029299 /* MessageList.swift in Sources */, + DD47E3DB26F3901B00029299 /* Channels.swift in Sources */, DDAF8C6926ED0D070058C060 /* deviceonly.pb.swift in Sources */, DD23A51326FEF5D500D9B90C /* MessageData.swift in Sources */, DD836AED26F858F900ABCC23 /* MeshData.swift in Sources */, @@ -471,7 +471,7 @@ DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */, DDAF8C6726ED0C8C0058C060 /* remote_hardware.pb.swift in Sources */, DDAF8C6526ED0A490058C060 /* channel.pb.swift in Sources */, - DD47E3DD26F390A000029299 /* MessageDetail.swift in Sources */, + DD47E3DD26F390A000029299 /* Messages.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticClientApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -641,7 +641,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.15; + MARKETING_VERSION = 1.16; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = NO; @@ -668,7 +668,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.15; + MARKETING_VERSION = 1.16; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = NO; diff --git a/MeshtasticClient/Model/MessageData.swift b/MeshtasticClient/Model/MessageData.swift index 1e0b9b0b..2164bbfd 100644 --- a/MeshtasticClient/Model/MessageData.swift +++ b/MeshtasticClient/Model/MessageData.swift @@ -22,11 +22,11 @@ class MessageData: ObservableObject { func load() { DispatchQueue.global(qos: .background).async { [weak self] in guard let data = try? Data(contentsOf: Self.fileURL) else { - #if DEBUG + //#if DEBUG DispatchQueue.main.async { self?.messages = MessageModel.data } - #endif + //#endif return } guard let messageList = try? JSONDecoder().decode([MessageModel].self, from: data) else { diff --git a/MeshtasticClient/Model/MessageModel.swift b/MeshtasticClient/Model/MessageModel.swift index dd1f662d..118202ff 100644 --- a/MeshtasticClient/Model/MessageModel.swift +++ b/MeshtasticClient/Model/MessageModel.swift @@ -45,8 +45,18 @@ extension MessageModel { [ // Put dev test data here MessageModel(messageId: 3773493287, messageTimeStamp: 1632407404, fromUserId: 4064715620, toUserId: 4294967295, fromUserLongName: "TLORA V1 #1", toUserLongName: "Unknown 1", fromUserShortName: "T#", toUserShortName: "U1", receivedACK: false, messagePayload: "I sent a super great message with amazing text", direction: "received"), - MessageModel(messageId: 3773493338, messageTimeStamp: 1632643652, fromUserId: 2930161432, toUserId: 4294967295, fromUserLongName: "TBEAM ARMY GREEN", toUserLongName: "Unknown 1", fromUserShortName: "TAG", toUserShortName: "U1", receivedACK: false, messagePayload: "It was the best message", direction: "received"), - MessageModel(messageId: 3773493338, messageTimeStamp: 1632643652, fromUserId: 2930161432, toUserId: 4294967295, fromUserLongName: "TBEAM ARMY GREEN", toUserLongName: "Unknown 1", fromUserShortName: "TAG", toUserShortName: "U1", receivedACK: false, messagePayload: "SwiftUI is great, but it has been lacking of specific native controls, even though that gets much better year by year. One of them was the text view. When SwiftUI was first released, it had no native equivalent of the text view; implementing a custom UIViewRepresentable type to contain UITextView was the only way to go. But since iOS 14, SwiftUI introduces TextEditor, a brand new view to write multi-line text.", direction: "received") + MessageModel(messageId: 3773493338, messageTimeStamp: 1632643652, fromUserId: 2930161432, toUserId: 4294967295, fromUserLongName: "TBEAM ARMY GREEN", toUserLongName: "Unknown 1", fromUserShortName: "T#", toUserShortName: "U1", receivedACK: false, messagePayload: "It was the best message", direction: "received"), + MessageModel(messageId: 3773493338, messageTimeStamp: 1632643652, fromUserId: 2930161432, toUserId: 4294967295, fromUserLongName: "TBEAM ARMY GREEN", toUserLongName: "Unknown 1", fromUserShortName: "TAG", toUserShortName: "U1", receivedACK: false, messagePayload: "SwiftUI is great, but it has been lacking of specific native controls, even though that gets much better year by year. One of them was the text view. When SwiftUI was first released, it had no native ", direction: "received"), + MessageModel(messageId: 3773493338, messageTimeStamp: 1632643652, fromUserId: 2930161432, toUserId: 4294967295, fromUserLongName: "TBEAM ARMY GREEN", toUserLongName: "Unknown 1", fromUserShortName: "TAG", toUserShortName: "U1", receivedACK: false, messagePayload: "One of them was the text view. When SwiftUI was first released, it had no native equivalent of the text view; implementing a custom UIViewRepresentable type to contain UITextView was the only way to g", direction: "received"), + MessageModel(messageId: 3773493338, messageTimeStamp: 1632407404, fromUserId: 2930161432, toUserId: 4294967295, fromUserLongName: "TBEAM ARMY GREEN", toUserLongName: "Unknown 1", fromUserShortName: "TAG", toUserShortName: "U1", receivedACK: false, messagePayload: "One of them was the text view. When SwiftUI was first released, it had no native equivalent of the text view; implementing a custom UIViewRepresentable type to contain UITextView was the only way to g", direction: "received"), + + MessageModel(messageId: 3773493338, messageTimeStamp: 1632643652, fromUserId: 2930161432, toUserId: 4294967295, fromUserLongName: "TBEAM ARMY GREEN", toUserLongName: "Unknown 1", fromUserShortName: "GVH", toUserShortName: "U1", receivedACK: false, messagePayload: "yo", direction: "received"), + + MessageModel(messageId: 3773493338, messageTimeStamp: 1632407404, fromUserId: 2930161432, toUserId: 4294967295, fromUserLongName: "TBEAM ARMY GREEN", toUserLongName: "Unknown 1", fromUserShortName: "GVH", toUserShortName: "U1", receivedACK: false, messagePayload: "yo", direction: "received") + + + + ] } } diff --git a/MeshtasticClient/Views/ContentView.swift b/MeshtasticClient/Views/ContentView.swift index cbc02241..3c5b27f1 100644 --- a/MeshtasticClient/Views/ContentView.swift +++ b/MeshtasticClient/Views/ContentView.swift @@ -17,7 +17,7 @@ struct ContentView: View { var body: some View { TabView(selection: $selection) { - MessageList() + Channels() .tabItem { Label("Messages", systemImage: "text.bubble") .symbolRenderingMode(.hierarchical) diff --git a/MeshtasticClient/Views/Helpers/MessageBubble.swift b/MeshtasticClient/Views/Helpers/MessageBubble.swift index 243103bd..d73b875c 100644 --- a/MeshtasticClient/Views/Helpers/MessageBubble.swift +++ b/MeshtasticClient/Views/Helpers/MessageBubble.swift @@ -13,6 +13,7 @@ struct MessageBubble: View { CircleText(text: shortName, color: isCurrentUser ? Color.blue : Color(.darkGray)).padding(.all, 5) VStack (alignment: .leading) { Text(contentMessage) + .textSelection(.enabled) .padding(10) .foregroundColor(.white) .background(isCurrentUser ? Color.blue : Color(.darkGray)) diff --git a/MeshtasticClient/Views/Messages/Channels.swift b/MeshtasticClient/Views/Messages/Channels.swift new file mode 100644 index 00000000..612c8825 --- /dev/null +++ b/MeshtasticClient/Views/Messages/Channels.swift @@ -0,0 +1,45 @@ +import Foundation +import SwiftUI +import CoreBluetooth + +struct Channels: View { + + var body: some View { + NavigationView { + + GeometryReader { bounds in + + NavigationLink(destination: Messages()) { + + List{ + + HStack { + + Image(systemName: "dial.max.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 70, height: 70) + .foregroundColor(Color.blue) + .symbolRenderingMode(.hierarchical) + .padding(.trailing) + + Text("Primary") + .font(.largeTitle) + }.padding() + } + } + } + .navigationTitle("Channels") + } + } +} + +struct MessageList_Previews: PreviewProvider { + static let meshData = MeshData() + + static var previews: some View { + Group { + Channels() + } + } +} diff --git a/MeshtasticClient/Views/Messages/MessageList.swift b/MeshtasticClient/Views/Messages/MessageList.swift deleted file mode 100644 index 3349bfe1..00000000 --- a/MeshtasticClient/Views/Messages/MessageList.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Foundation -import SwiftUI -import CoreBluetooth - -struct MessageList: View { - - @State var typingMessage: String = "" - - @EnvironmentObject var bleManager: BLEManager - - var body: some View { - NavigationView { - - GeometryReader { bounds in - - NavigationLink(destination: MessageDetail()) { - - List{ - - HStack { - - Image(systemName: "dial.max.fill") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: bounds.size.width / 7, height: bounds.size.height / 7) - .foregroundColor(Color.blue) - .symbolRenderingMode(.hierarchical) - .padding(.trailing) - - Text("Primary") - .font(.largeTitle) - }.padding([.leading, .trailing]) - } - } - } - .navigationTitle("Message Channels") - .navigationBarItems(trailing: - - ZStack { - - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedNode != nil) ? bleManager.connectedNode.user.longName : ((bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.name : "Unknown") ?? "Unknown") - - } - ) - }.navigationViewStyle(StackNavigationViewStyle()) - } -} - -struct MessageList_Previews: PreviewProvider { - static let meshData = MeshData() - - static var previews: some View { - Group { - MessageList() - } - } -} - -func sendMessage() { - //chatHelper.sendMessage(Message(content: typingMessage, user: DataSource.secondUser)) - // typingMessage = "" - } diff --git a/MeshtasticClient/Views/Messages/MessageDetail.swift b/MeshtasticClient/Views/Messages/Messages.swift similarity index 60% rename from MeshtasticClient/Views/Messages/MessageDetail.swift rename to MeshtasticClient/Views/Messages/Messages.swift index 1eb90f4a..9574a96b 100644 --- a/MeshtasticClient/Views/Messages/MessageDetail.swift +++ b/MeshtasticClient/Views/Messages/Messages.swift @@ -2,20 +2,24 @@ import SwiftUI import MapKit import CoreLocation -struct MessageDetail: View { +struct Messages: View { enum Field: Hashable { case messageText } + // Keyboard State @State var typingMessage: String = "" + @State private var totalBytes = 0 + @State private var lastTypingMessage = "" @FocusState private var focusedField: Field? - @ObservedObject var messageData: MessageData = MessageData() - @EnvironmentObject var bleManager: BLEManager - @Namespace var topId @Namespace var bottomId + // Message Data and Bluetooth + @ObservedObject var messageData: MessageData = MessageData() + @EnvironmentObject var bleManager: BLEManager + var body: some View { GeometryReader { bounds in @@ -25,10 +29,8 @@ struct MessageDetail: View { ScrollViewReader { scrollView in ScrollView { - Text("Hidden Top Anchor") - .hidden() - .frame(height: 0) - .id(topId) + + Text("Hidden Top Anchor").hidden().frame(height: 0).id(topId) ForEach(messageData.messages.sorted(by: { $0.messageTimestamp < $1.messageTimestamp })) { message in @@ -36,68 +38,80 @@ struct MessageDetail: View { } .onAppear(perform: { scrollView.scrollTo(bottomId) } ) - Text("Hidden Bottom Anchor") - .hidden() - .frame(height: 0) - .id(bottomId) + Text("Hidden Bottom Anchor").hidden().frame(height: 0).id(bottomId) } - .padding([.top, .leading]) + .padding(.horizontal) } - HStack { + + HStack (alignment: .top) { + + ZStack { + + TextEditor(text: $typingMessage) + .onChange(of: typingMessage, perform: { value in + + let size = value.utf8.count + totalBytes = size + if totalBytes <= 200 { + // Allow the user to type + lastTypingMessage = typingMessage + } + else { + // Set the message back and remove the bytes over the count + self.typingMessage = lastTypingMessage + } + }) + .keyboardType(.default) + .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: sendMessage) { + Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue) + } + + } + .padding(.all, 15) + HStack (alignment: .top ) { if focusedField != nil { Button("Dismiss Keyboard") { focusedField = nil } - .fixedSize() - .frame(height: 15, alignment: .center) - .padding(.top, 10) + .font(.subheadline) + Spacer() + ProgressView("Bytes: \(totalBytes) / 200", value: Double(totalBytes), total: 200) + .frame(width: 130) + .padding(.bottom, 7) + .font(.subheadline) + .accentColor(Color.blue) } } - HStack (alignment: .top) { - - ZStack { - - TextEditor(text: $typingMessage) - .onChange(of: typingMessage, perform: { value in - let size = value.utf8.count - if size >= 200 { - print("too big!") - } - print(size) - }) - .padding(.horizontal) - .focused($focusedField, equals: .messageText) - .multilineTextAlignment(.leading) - .frame(minHeight: 120, maxHeight: 120) - - - Text(typingMessage).opacity(0).padding(.all, 2) - - } - .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 2)) - .padding(.top) - - Button(action: sendMessage) { - Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue) - } - .padding(.top) - - }.padding([.leading, .bottom]) + .padding(.horizontal) } } - .navigationTitle("CHANNEL - Primary") + .navigationTitle("Channel - Primary") .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedNode != nil) ? bleManager.connectedNode.user.longName : ((bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.name : "Unknown") ?? "Unknown") - } ) - .onAppear{ + .onAppear { messageData.load() } } } +func sendMessage() { + //chatHelper.sendMessage(Message(content: typingMessage, user: DataSource.secondUser)) + // typingMessage = "" + }