mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Clean up messages detail view, automatically scroll to bottom of message list when loading
This commit is contained in:
parent
2c13a9ed95
commit
029ac8556f
11 changed files with 217 additions and 56 deletions
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; };
|
||||
DD23A51326FEF5D500D9B90C /* MessageData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A51226FEF5D500D9B90C /* MessageData.swift */; };
|
||||
DD47E3CC26F0E51D00029299 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CB26F0E51D00029299 /* NodeDetail.swift */; };
|
||||
DD47E3CE26F103C600029299 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeList.swift */; };
|
||||
DD47E3D026F1073F00029299 /* NodeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CF26F1073F00029299 /* NodeRow.swift */; };
|
||||
|
|
@ -64,6 +65,7 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
|
||||
DD23A51226FEF5D500D9B90C /* MessageData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageData.swift; sourceTree = "<group>"; };
|
||||
DD47E3CB26F0E51D00029299 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = "<group>"; };
|
||||
DD47E3CD26F103C600029299 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = "<group>"; };
|
||||
DD47E3CF26F1073F00029299 /* NodeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeRow.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -260,6 +262,7 @@
|
|||
DD836AEC26F858F900ABCC23 /* MeshData.swift */,
|
||||
DD836AEE26F85D8D00ABCC23 /* NodeInfoModel.swift */,
|
||||
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */,
|
||||
DD23A51226FEF5D500D9B90C /* MessageData.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -449,6 +452,7 @@
|
|||
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
|
||||
DD47E3DB26F3901B00029299 /* MessageList.swift in Sources */,
|
||||
DDAF8C6926ED0D070058C060 /* deviceonly.pb.swift in Sources */,
|
||||
DD23A51326FEF5D500D9B90C /* MessageData.swift in Sources */,
|
||||
DD836AED26F858F900ABCC23 /* MeshData.swift in Sources */,
|
||||
DDAF8C6B26ED0DD80058C060 /* environmental_measurement.pb.swift in Sources */,
|
||||
DD90860C26F684AF00DC5189 /* BatteryIcon.swift in Sources */,
|
||||
|
|
@ -637,10 +641,11 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 1.15;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
|
|
@ -663,10 +668,11 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 1.15;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ struct MeshtasticClientApp: App {
|
|||
@ObservedObject private var meshData: MeshData = MeshData()
|
||||
@ObservedObject private var bleManager: BLEManager = BLEManager()
|
||||
|
||||
//@ObservedObject var meshData: MeshData
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
|
|
|
|||
40
MeshtasticClient/Model/MessageData.swift
Normal file
40
MeshtasticClient/Model/MessageData.swift
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import Foundation
|
||||
|
||||
class MessageData: ObservableObject {
|
||||
|
||||
private static var documentsFolder: URL {
|
||||
do {
|
||||
return try FileManager.default.url(for: .documentDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: nil,
|
||||
create: true)
|
||||
} catch {
|
||||
fatalError("Can't find documents directory.")
|
||||
}
|
||||
}
|
||||
|
||||
private static var fileURL: URL {
|
||||
return documentsFolder.appendingPathComponent("messages.data")
|
||||
}
|
||||
|
||||
@Published var messages: [MessageModel] = []
|
||||
|
||||
func load() {
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||
guard let data = try? Data(contentsOf: Self.fileURL) else {
|
||||
#if DEBUG
|
||||
DispatchQueue.main.async {
|
||||
self?.messages = MessageModel.data
|
||||
}
|
||||
#endif
|
||||
return
|
||||
}
|
||||
guard let messageList = try? JSONDecoder().decode([MessageModel].self, from: data) else {
|
||||
fatalError("Can't decode saved node data.")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self?.messages = messageList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,13 +6,13 @@
|
|||
//
|
||||
import Foundation
|
||||
|
||||
struct MessageModel : Identifiable
|
||||
struct MessageModel : Identifiable, Codable
|
||||
{
|
||||
let id: UUID
|
||||
var messageId: UInt32
|
||||
var messageTimestamp: Int64
|
||||
var fromUserId: String
|
||||
var toUserId: String
|
||||
var fromUserId: UInt32
|
||||
var toUserId: UInt32
|
||||
var fromUserLongName: String
|
||||
var toUserLongName: String
|
||||
var fromUserShortName: String
|
||||
|
|
@ -21,7 +21,7 @@ struct MessageModel : Identifiable
|
|||
var messagePayload: String
|
||||
var direction: String
|
||||
|
||||
init(id: UUID = UUID(), messageId: UInt32, messageTimeStamp: Int64, fromUserId: String, toUserId: String, fromUserLongName: String, toUserLongName: String, fromUserShortName: String, toUserShortName: String, receivedACK: Bool, messagePayload: String, direction: String)
|
||||
init(id: UUID = UUID(), messageId: UInt32, messageTimeStamp: Int64, fromUserId: UInt32, toUserId: UInt32, fromUserLongName: String, toUserLongName: String, fromUserShortName: String, toUserShortName: String, receivedACK: Bool, messagePayload: String, direction: String)
|
||||
{
|
||||
self.id = id
|
||||
self.messageId = messageId
|
||||
|
|
@ -38,3 +38,51 @@ struct MessageModel : Identifiable
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension MessageModel {
|
||||
|
||||
static var data: [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")
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageModel {
|
||||
struct Data {
|
||||
var id: UUID
|
||||
var messageId: UInt32
|
||||
var messageTimestamp: Int64
|
||||
var fromUserId: UInt32
|
||||
var toUserId: UInt32
|
||||
var fromUserLongName: String
|
||||
var toUserLongName: String
|
||||
var fromUserShortName: String
|
||||
var toUserShortName: String
|
||||
var receivedACK: Bool
|
||||
var messagePayload: String
|
||||
var direction: String
|
||||
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
return Data(id: id, messageId: messageId, messageTimestamp: messageTimestamp, fromUserId: fromUserId, toUserId: toUserId, fromUserLongName: fromUserLongName, toUserLongName: toUserLongName, fromUserShortName: fromUserShortName, toUserShortName: toUserShortName, receivedACK: receivedACK, messagePayload: messagePayload, direction: direction)
|
||||
}
|
||||
|
||||
mutating func update(from data: Data) {
|
||||
messageId = data.messageId
|
||||
messageTimestamp = data.messageTimestamp
|
||||
fromUserId = data.fromUserId
|
||||
toUserId = data.toUserId
|
||||
fromUserLongName = data.fromUserLongName
|
||||
toUserLongName = data.toUserLongName
|
||||
fromUserShortName = data.fromUserShortName
|
||||
toUserShortName = data.toUserShortName
|
||||
receivedACK = data.receivedACK
|
||||
messagePayload = data.messagePayload
|
||||
direction = data.direction
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@ struct ContentView: View {
|
|||
var body: some View {
|
||||
|
||||
TabView(selection: $selection) {
|
||||
//MessageList()
|
||||
// .tabItem {
|
||||
// Label("Messages", systemImage: "text.bubble")
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// }
|
||||
// .tag(Tab.messages)
|
||||
MessageList()
|
||||
.tabItem {
|
||||
Label("Messages", systemImage: "text.bubble")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.tag(Tab.messages)
|
||||
NodeList()
|
||||
.tabItem {
|
||||
Label("Nodes", systemImage: "flipphone")
|
||||
|
|
|
|||
|
|
@ -7,18 +7,27 @@ struct MessageBubble: View {
|
|||
var shortName: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: isCurrentUser ? .leading : .trailing) {
|
||||
HStack {
|
||||
VStack {
|
||||
HStack (alignment: .top) {
|
||||
|
||||
CircleText(text: shortName, color: isCurrentUser ? Color.blue : Color(.darkGray)).padding(.all, 5)
|
||||
|
||||
VStack (alignment: .leading) {
|
||||
Text(contentMessage)
|
||||
.padding(10)
|
||||
.foregroundColor(.white)
|
||||
.background(isCurrentUser ? Color.blue : Color(.darkGray))
|
||||
.cornerRadius(10)
|
||||
HStack (spacing: 4) {
|
||||
let messageDate = Date(timeIntervalSince1970: TimeInterval(time))
|
||||
|
||||
Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray)
|
||||
Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray)
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
Spacer()
|
||||
}.padding(isCurrentUser ? .leading : .trailing, 70)
|
||||
}
|
||||
|
||||
}.padding(.bottom, 1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,42 +4,100 @@ import CoreLocation
|
|||
|
||||
struct MessageDetail: View {
|
||||
|
||||
enum Field: Hashable {
|
||||
case messageText
|
||||
}
|
||||
|
||||
@State var typingMessage: String = ""
|
||||
@FocusState private var focusedField: Field?
|
||||
@ObservedObject var messageData: MessageData = MessageData()
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
@Namespace var topId
|
||||
@Namespace var bottomId
|
||||
|
||||
var body: some View {
|
||||
// NavigationView {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
ScrollView {
|
||||
|
||||
GeometryReader { bounds in
|
||||
|
||||
VStack {
|
||||
|
||||
ScrollViewReader { scrollView in
|
||||
|
||||
MessageBubble(contentMessage: "I sent a super great message with amazing text", isCurrentUser: true, time: 1, shortName: "GVH")
|
||||
MessageBubble(contentMessage: "It was amazing to read such a fantastical text", isCurrentUser: false, time: 1, shortName: "RS1")
|
||||
MessageBubble(contentMessage: "It was the best message", isCurrentUser: false, time: 1, shortName: "RDN")
|
||||
MessageBubble(contentMessage: "This is a terse response to an amazing text", isCurrentUser: true, time: 1, shortName: "GVH")
|
||||
MessageBubble(contentMessage: "yo", isCurrentUser: true, time: 1, shortName: "GVH")
|
||||
MessageBubble(contentMessage: "I sent a super great message with amazing text", isCurrentUser: true, time: 1, shortName: "GVH")
|
||||
MessageBubble(contentMessage: "It was amazing to read such a fantastical text", isCurrentUser: false, time: 1, shortName: "RS1")
|
||||
MessageBubble(contentMessage: "It was the best message", isCurrentUser: false, time: 1, shortName: "RDN")
|
||||
MessageBubble(contentMessage: "This is a terse response to an amazing text", isCurrentUser: true, time: 1, shortName: "GVH")
|
||||
MessageBubble(contentMessage: "yo", isCurrentUser: true, time: 1, shortName: "GVH")
|
||||
|
||||
|
||||
|
||||
|
||||
}.padding([.top, .leading])
|
||||
HStack (alignment: .bottom) {
|
||||
|
||||
TextField("Message", text: $typingMessage)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.frame(minHeight: CGFloat(30))
|
||||
Button(action: sendMessage) {
|
||||
Image(systemName: "arrow.up.circle.fill").font(.title).foregroundColor(.blue)
|
||||
ScrollView {
|
||||
Text("Hidden Top Anchor")
|
||||
.hidden()
|
||||
.frame(height: 0)
|
||||
.id(topId)
|
||||
|
||||
ForEach(messageData.messages.sorted(by: { $0.messageTimestamp < $1.messageTimestamp })) { message in
|
||||
|
||||
MessageBubble(contentMessage: message.messagePayload, isCurrentUser: false, time: Int32(message.messageTimestamp), shortName: message.fromUserShortName)
|
||||
}
|
||||
}.padding(5)
|
||||
.onAppear(perform: { scrollView.scrollTo(bottomId) } )
|
||||
|
||||
Text("Hidden Bottom Anchor")
|
||||
.hidden()
|
||||
.frame(height: 0)
|
||||
.id(bottomId)
|
||||
}
|
||||
.padding([.top, .leading])
|
||||
}
|
||||
HStack {
|
||||
|
||||
if focusedField != nil {
|
||||
Button("Dismiss Keyboard") {
|
||||
focusedField = nil
|
||||
}
|
||||
.fixedSize()
|
||||
.frame(height: 15, alignment: .center)
|
||||
.padding(.top, 10)
|
||||
}
|
||||
}
|
||||
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])
|
||||
}
|
||||
.navigationTitle("CHANNEL - Primary")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
//}
|
||||
//.navigationViewStyle//(StackNavigationViewStyle())
|
||||
}
|
||||
.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{
|
||||
messageData.load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,16 +7,15 @@ struct MessageList: View {
|
|||
@State var typingMessage: String = ""
|
||||
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@EnvironmentObject var meshData: MeshData
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
|
||||
GeometryReader { bounds in
|
||||
|
||||
List{
|
||||
NavigationLink(destination: MessageDetail()) {
|
||||
|
||||
NavigationLink(destination: MessageDetail()) {
|
||||
List{
|
||||
|
||||
HStack {
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ struct NodeDetail: View {
|
|||
HStack {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text("AKA").font(.title2)
|
||||
Text("AKA").font(.title2).fixedSize()
|
||||
CircleText(text: node.user.shortName, color: Color.blue)
|
||||
.offset(y:10)
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ struct NodeDetail: View {
|
|||
.font(.title)
|
||||
.foregroundColor(.blue)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("SNR").font(.title2)
|
||||
Text("SNR").font(.title2).fixedSize()
|
||||
Text(String(node.snr ?? 0))
|
||||
.font(.title2)
|
||||
.foregroundColor(.gray)
|
||||
|
|
@ -77,7 +77,7 @@ struct NodeDetail: View {
|
|||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
BatteryIcon(batteryLevel: node.position.batteryLevel, font: .title, color: Color.blue)
|
||||
Text("Battery").font(.title2)
|
||||
Text("Battery").font(.title2).fixedSize()
|
||||
Text(String(node.position.batteryLevel!) + "%")
|
||||
.font(.title2)
|
||||
.foregroundColor(.gray)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@ struct NodeMap: View {
|
|||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
|
||||
.onAppear{
|
||||
meshData.load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ struct NodeRow: View {
|
|||
HStack() {
|
||||
|
||||
CircleText(text: node.user.shortName, color: Color.blue).offset(y: 1).padding(.trailing, 5)
|
||||
Text(node.user.longName).font(.title)
|
||||
Text(node.user.longName).font(.title2)
|
||||
}
|
||||
.padding([.trailing])
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue