Meshtastic-Apple/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift
Garth Vander Houwen 88ed168725
2.7.1 Working Changes (#1392)
* Bump version

* Offset for map controls on the mesh map

* Only mark messages as read if they are unread (#1388)

* Only mark messages as read if they are unread

* More cheap optimizations

* Fix map control positions on the route recorder

* Add seperate state variable for delete all channel messges button since the channelSelection is being used for navigation

* Use a seperate state variable to track what user messages are being deleted for as userSelection is being used for navigation

* Get the ringtone if external notifications is enabled

* Fix RTTTL typo

* Dont show modem lights popover if we are on macOS 26 cause it crashes

* Fix annoying connect bottom background bug

* Update mesh map detents

* Move divider inside of the hstack keyboard toolbar

* Update Meshtastic/Helpers/MeshPackets.swift

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Remove extra environment variable that is not getting used

---------

Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-12 21:45:05 -07:00

145 lines
3.7 KiB
Swift

import SwiftUI
import OSLog
import DatadogSessionReplay
struct TextMessageField: View {
static let maxbytes = 200
@EnvironmentObject var accessoryManager: AccessoryManager
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.dismiss) var dismiss
let destination: MessageDestination
@Binding var replyMessageId: Int64
@FocusState.Binding var isFocused: Bool
@State private var typingMessage: String = ""
@State private var totalBytes = 0
@State private var sendPositionWithMessage = false
var body: some View {
SessionReplayPrivacyView(textAndInputPrivacy: .maskAllInputs) {
VStack(spacing: 0) {
HStack(alignment: .bottom) {
if replyMessageId != 0 || isFocused {
Button {
withAnimation(.easeInOut(duration: 0.2)) {
replyMessageId = 0
}
isFocused = false
} label: {
Image(systemName: "x.circle.fill")
.font(.largeTitle)
}
if replyMessageId != 0 {
Text("Reply")
.padding(.bottom, 10)
}
}
TextField("Message", text: $typingMessage, axis: .vertical)
.padding(10)
.background(
Capsule()
.strokeBorder(.tertiary, lineWidth: 1)
.background(Capsule().fill(Color(.systemBackground)))
)
.clipShape(Capsule())
.onChange(of: typingMessage) { _, value in
totalBytes = value.utf8.count
while totalBytes > Self.maxbytes {
typingMessage = String(typingMessage.dropLast())
totalBytes = typingMessage.utf8.count
}
}
.keyboardType(.default)
.focused($isFocused)
.multilineTextAlignment(.leading)
.onSubmit {
#if targetEnvironment(macCatalyst)
sendMessage()
#endif
}
.foregroundColor(.primary)
if !typingMessage.isEmpty {
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle.fill")
.font(.largeTitle)
.foregroundColor(.accentColor)
}
}
}
.padding(15)
if isFocused {
Divider()
HStack {
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
Spacer()
RequestPositionButton(action: requestPosition)
Spacer()
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes)
}
.padding(.horizontal, 15)
.padding(.vertical, 10)
.background(.bar)
}
}
}
}
private func requestPosition() {
let userLongName = accessoryManager.activeConnection?.device.longName ?? "Unknown"
sendPositionWithMessage = true
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
}
private func sendMessage() {
Task {
do {
try await accessoryManager.sendMessage(
message: typingMessage,
toUserNum: destination.userNum,
channel: destination.channelNum,
isEmoji: false,
replyID: replyMessageId)
typingMessage = ""
isFocused = false
replyMessageId = 0
if sendPositionWithMessage {
try await accessoryManager.sendPosition(
channel: destination.channelNum,
destNum: destination.positionDestNum,
wantResponse: destination.wantPositionResponse
)
Logger.mesh.info("Location Sent")
}
} catch {
Logger.mesh.info("Error sending message")
}
}
}
}
private extension MessageDestination {
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 positionDestNum: Int64 {
switch self {
case let .user(user): return user.num
case .channel: return Int64(Constants.maximumNodeNum)
}
}
var wantPositionResponse: Bool {
switch self {
case .user: return true
case .channel: return false
}
}
}