fix: slow typing speed when lots of messages

Refactors both the channel and user message views to isolate typing state which
prevents excessive re-rendering of large message lists on every new character
typed. Also consolidates typing view code of both lists into the new
TextMessageField and related sub views.
This commit is contained in:
Austin Payne 2024-02-05 23:31:54 -07:00 committed by Austin Payne
parent 416d5e5f41
commit 3882add56a
8 changed files with 275 additions and 250 deletions

View file

@ -10,8 +10,12 @@
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; };
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; };
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; };
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 */; };
D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */; };
D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */; };
D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */; };
DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */; };
DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; };
DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; };
@ -226,7 +230,11 @@
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = "<group>"; };
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = "<group>"; };
A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = "<group>"; };
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = "<group>"; };
D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSize.swift; sourceTree = "<group>"; };
D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; };
D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestPositionButton.swift; sourceTree = "<group>"; };
DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = "<group>"; };
DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = "<group>"; };
DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = "<group>"; };
@ -487,6 +495,17 @@
path = Custom;
sourceTree = "<group>";
};
D9C9839E2B79D0C600BDBE6A /* TextMessageField */ = {
isa = PBXGroup;
children = (
B3E905B02B71F7F300654D07 /* TextMessageField.swift */,
D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */,
D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */,
D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */,
);
path = TextMessageField;
sourceTree = "<group>";
};
DD007BB12AA59B9A00F5FA12 /* CoreData */ = {
isa = PBXGroup;
children = (
@ -818,6 +837,7 @@
DDC2E18B26CE25A70042C5E4 /* Messages */ = {
isa = PBXGroup;
children = (
D9C9839E2B79D0C600BDBE6A /* TextMessageField */,
DDB8F4132A9EE5F000230ECE /* ChannelList.swift */,
DD798B062915928D005217CD /* ChannelMessageList.swift */,
DDB8F40F2A9EE5B400230ECE /* Messages.swift */,
@ -1162,6 +1182,7 @@
DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */,
DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */,
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */,
DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */,
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
@ -1238,6 +1259,7 @@
DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */,
DDDE5A1029AFE69700490C6C /* MeshActivityAttributes.swift in Sources */,
DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */,
D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */,
DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */,
DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */,
DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */,
@ -1271,6 +1293,7 @@
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */,
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */,
D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */,
DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */,
DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */,
DD77093F2AA1B146007A8BF0 /* UIColor.swift in Sources */,
@ -1281,6 +1304,7 @@
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */,
DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */,
DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */,
B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */,
DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */,
DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */,
DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */,

View file

@ -38,7 +38,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var timeoutTimerCount = 0
var positionTimer: Timer?
var lastPosition: CLLocationCoordinate2D?
let emptyNodeNum: UInt32 = 4294967295
static let emptyNodeNum: UInt32 = 4294967295
let mqttManager = MqttClientProxyManager.shared
var wantRangeTestPackets = false
var wantStoreAndForwardPackets = false
@ -865,7 +865,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if toUserNum > 0 {
meshPacket.to = UInt32(toUserNum)
} else {
meshPacket.to = emptyNodeNum
meshPacket.to = Self.emptyNodeNum
}
meshPacket.channel = UInt32(channel)
meshPacket.from = UInt32(fromUserNum)
@ -912,7 +912,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var success = false
let fromNodeNum = UInt32(connectedPeripheral.num)
var meshPacket = MeshPacket()
meshPacket.to = emptyNodeNum
meshPacket.to = Self.emptyNodeNum
meshPacket.from = fromNodeNum
meshPacket.wantAck = true
var dataMessage = DataMessage()

View file

@ -9,27 +9,18 @@ import SwiftUI
import CoreData
struct ChannelMessageList: View {
@StateObject var appState = AppState.shared
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
enum Field: Hashable {
case messageText
}
// Keyboard State
@State var typingMessage: String = ""
@State private var totalBytes = 0
var maxbytes = 228
@FocusState var focusedField: Field?
@FocusState var messageFieldFocused: Bool
@ObservedObject var myInfo: MyInfoEntity
@ObservedObject var channel: ChannelEntity
@State var showDeleteMessageAlert = false
@State private var deleteMessageId: Int64 = 0
@State private var replyMessageId: Int64 = 0
@State private var sendPositionWithMessage: Bool = false
@AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1
var body: some View {
@ -115,7 +106,7 @@ struct ChannelMessageList: View {
}
Button(action: {
self.replyMessageId = message.messageId
self.focusedField = .messageText
self.messageFieldFocused = true
print("I want to reply to \(message.messageId)")
}) {
Text("reply")
@ -288,134 +279,14 @@ struct ChannelMessageList: View {
}
})
}
#if targetEnvironment(macCatalyst)
HStack {
Spacer()
Button {
let bell = "🔔 Alert Bell Character! \u{7}"
print(bell)
typingMessage += bell
} label: {
Text("Alert Bell")
Image(systemName: "bell.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
}
Spacer()
Button {
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
sendPositionWithMessage = true
typingMessage += "📍 " + userLongName + " has shared their position with you."
} label: {
Text("share.position")
Image(systemName: "mappin.and.ellipse")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
}
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
.frame(width: 130)
.padding(5)
.font(.subheadline)
.accentColor(.accentColor)
.padding(.trailing)
TextMessageField(
destination: .channel(channel.index),
replyMessageId: $replyMessageId,
isFocused: $messageFieldFocused
) {
context.refresh(channel, mergeChanges: true)
}
#endif
HStack(alignment: .top) {
ZStack {
TextField("message", text: $typingMessage, axis: .vertical)
.onChange(of: typingMessage, perform: { value in
totalBytes = value.utf8.count
// Only mess with the value if it is too big
if totalBytes > maxbytes {
let firstNBytes = Data(typingMessage.utf8.prefix(maxbytes))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the message back to the last place where it was the right size
typingMessage = maxBytesString
} else {
print("not a valid UTF-8 sequence")
}
}
})
.keyboardType(.default)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("dismiss.keyboard") {
focusedField = nil
}
.font(.subheadline)
Spacer()
Button {
let bell = "🔔 Alert Bell Character! \u{7}"
print(bell)
typingMessage += bell
} label: {
Text("Alert")
Image(systemName: "bell.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
}
Spacer()
Button {
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
sendPositionWithMessage = true
typingMessage = "📍 " + userLongName + " has shared their position with you."
} label: {
Image(systemName: "mappin.and.ellipse")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
}
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
.frame(width: 130)
.padding(5)
.font(.subheadline)
.accentColor(.accentColor)
}
}
.padding(.horizontal, 8)
.focused($focusedField, equals: .messageText)
.multilineTextAlignment(.leading)
.frame(minHeight: 50)
.keyboardShortcut(.defaultAction)
.onSubmit {
#if targetEnvironment(macCatalyst)
if bleManager.sendMessage(message: typingMessage, toUserNum: 0, channel: channel.index, isEmoji: false, replyID: replyMessageId) {
typingMessage = ""
focusedField = nil
replyMessageId = 0
if sendPositionWithMessage {
if bleManager.sendPosition(channel: Int32(channel.index), destNum: Int64(bleManager.emptyNodeNum), wantResponse: false) {
print("Location Sent")
}
}
}
#endif
}
Text(typingMessage).opacity(0).padding(.all, 0)
}
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
.padding(.bottom, 15)
Button(action: {
if bleManager.sendMessage(message: typingMessage, toUserNum: 0, channel: channel.index, isEmoji: false, replyID: replyMessageId) {
typingMessage = ""
focusedField = nil
replyMessageId = 0
if sendPositionWithMessage {
if bleManager.sendPosition(channel: Int32(channel.index), destNum: Int64(bleManager.emptyNodeNum), wantResponse: false) {
print("Location Sent")
}
}
}
}) {
Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.accentColor)
}
}
.padding(.all, 15)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {

View file

@ -0,0 +1,21 @@
import SwiftUI
struct AlertButton: View {
let action: () -> Void
var body: some View {
Button(action: action) {
Text("Alert")
Image(systemName: "bell.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large)
.foregroundColor(.accentColor)
}
}
}
struct AlertButtonPreview: PreviewProvider {
static var previews: some View {
AlertButton {}
}
}

View file

@ -0,0 +1,20 @@
import SwiftUI
struct RequestPositionButton: View {
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: "mappin.and.ellipse")
.symbolRenderingMode(.hierarchical)
.imageScale(.large)
.foregroundColor(.accentColor)
}
}
}
struct RequestPositionButtonPreview: PreviewProvider {
static var previews: some View {
RequestPositionButton {}
}
}

View file

@ -0,0 +1,170 @@
import SwiftUI
struct TextMessageField: View {
static let maxbytes = 228
@EnvironmentObject var bleManager: BLEManager
let destination: Destination
@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
@State private var sendPositionWithMessage = false
var body: some View {
#if targetEnvironment(macCatalyst)
HStack {
if destination.showAlertButton {
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
}
Spacer()
RequestPositionButton(action: requestPosition)
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
}
#endif
HStack(alignment: .top) {
ZStack {
TextField("message", text: $typingMessage, axis: .vertical)
.onChange(of: typingMessage, perform: { value in
totalBytes = value.utf8.count
// Only mess with the value if it is too big
if totalBytes > Self.maxbytes {
let firstNBytes = Data(typingMessage.utf8.prefix(Self.maxbytes))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the message back to the last place where it was the right size
typingMessage = maxBytesString
} else {
print("not a valid UTF-8 sequence")
}
}
})
.keyboardType(.default)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("dismiss.keyboard") {
isFocused = false
}
.font(.subheadline)
if destination.showAlertButton {
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
}
Spacer()
RequestPositionButton(action: requestPosition)
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes)
}
}
.padding(.horizontal, 8)
.focused($isFocused)
.multilineTextAlignment(.leading)
.frame(minHeight: 50)
.keyboardShortcut(.defaultAction)
.onSubmit {
#if targetEnvironment(macCatalyst)
sendMessage()
#endif
}
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(.accentColor)
}
}
.padding(.all, 15)
}
private func requestPosition() {
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
sendPositionWithMessage = true
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
}
private func sendMessage() {
let messageSent = bleManager.sendMessage(
message: typingMessage,
toUserNum: destination.userNum,
channel: destination.channelNum,
isEmoji: false,
replyID: replyMessageId
)
if messageSent {
typingMessage = ""
isFocused = false
replyMessageId = 0
onSubmit()
if sendPositionWithMessage {
let positionSent = bleManager.sendPosition(
channel: destination.channelNum,
destNum: destination.positionDestNum,
wantResponse: destination.wantPositionResponse
)
if positionSent {
print("Location Sent")
}
}
}
}
}
private extension TextMessageField.Destination {
var positionShareMessage: String {
switch self {
case .user: return "has shared their position and requested a response with your position"
case .channel: return "has shared their position with you"
}
}
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 .channel: return Int64(BLEManager.emptyNodeNum)
}
}
var showAlertButton: Bool {
switch self {
case .user: return false
case .channel: return true
}
}
var wantPositionResponse: Bool {
switch self {
case .user: return true
case .channel: return false
}
}
}

View file

@ -0,0 +1,20 @@
import SwiftUI
struct TextMessageSize: View {
let maxbytes: Int
let totalBytes: Int
var body: some View {
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
.frame(width: 130)
.padding(5)
.font(.subheadline)
.accentColor(.accentColor)
}
}
struct TextMessageSizePreview: PreviewProvider {
static var previews: some View {
TextMessageSize(maxbytes: 228, totalBytes: 100)
}
}

View file

@ -9,25 +9,17 @@ import SwiftUI
import CoreData
struct UserMessageList: View {
@StateObject var appState = AppState.shared
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
enum Field: Hashable {
case messageText
}
// Keyboard State
@State var typingMessage: String = ""
@State private var totalBytes = 0
var maxbytes = 228
@FocusState var focusedField: Field?
@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
@State private var sendPositionWithMessage: Bool = false
var body: some View {
VStack {
@ -88,7 +80,7 @@ struct UserMessageList: View {
}
Button(action: {
self.replyMessageId = message.messageId
self.focusedField = .messageText
self.messageFieldFocused = true
print("I want to reply to \(message.messageId)")
}) {
Text("reply")
@ -265,107 +257,14 @@ struct UserMessageList: View {
}
})
}
#if targetEnvironment(macCatalyst)
HStack {
Spacer()
Button {
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
sendPositionWithMessage = true
typingMessage = "📍 " + userLongName + " has shared their position and requested a response with your position."
} label: {
Text("share.position")
Image(systemName: "mappin.and.ellipse")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
}
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
.frame(width: 130)
.padding(5)
.font(.subheadline)
.accentColor(.accentColor)
.padding(.trailing)
}
#endif
HStack(alignment: .top) {
ZStack {
TextField("message", text: $typingMessage, axis: .vertical)
.onChange(of: typingMessage, perform: { value in
totalBytes = value.utf8.count
// Only mess with the value if it is too big
if totalBytes > maxbytes {
let firstNBytes = Data(typingMessage.utf8.prefix(maxbytes))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the message back to the last place where it was the right size
typingMessage = maxBytesString
} else {
print("not a valid UTF-8 sequence")
}
}
})
.keyboardType(.default)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("dismiss.keyboard") {
focusedField = nil
}
.font(.subheadline)
Spacer()
Button {
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
sendPositionWithMessage = true
typingMessage = "📍 " + userLongName + " has shared their position and requested a response with your position."
} label: {
Image(systemName: "mappin.and.ellipse")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
}
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
.frame(width: 130)
.padding(5)
.font(.subheadline)
.accentColor(.accentColor)
}
}
.padding(.horizontal, 8)
.focused($focusedField, equals: .messageText)
.multilineTextAlignment(.leading)
.frame(minHeight: 50)
.keyboardShortcut(.defaultAction)
.onSubmit {
#if targetEnvironment(macCatalyst)
if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, channel: 0, isEmoji: false, replyID: replyMessageId) {
typingMessage = ""
focusedField = nil
replyMessageId = 0
if sendPositionWithMessage {
if bleManager.sendPosition(channel: 0, destNum: user.num, wantResponse: true) {
print("Location Sent")
}
}
}
#endif
}
Text(typingMessage).opacity(0).padding(.all, 0)
}
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
.padding(.bottom, 15)
Button(action: {
if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, channel: 0, isEmoji: false, replyID: replyMessageId) {
typingMessage = ""
focusedField = nil
replyMessageId = 0
if sendPositionWithMessage {
if bleManager.sendPosition(channel: 0, destNum: user.num, wantResponse: true) {
print("Location Sent")
}
}
}
}) {
Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.accentColor)
}
TextMessageField(
destination: .user(user.num),
replyMessageId: $replyMessageId,
isFocused: $messageFieldFocused
) {
context.refresh(user, mergeChanges: true)
}
.padding(.all, 15)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {