mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
improvement: add MessageText views
This commit is contained in:
parent
5134e1d65d
commit
d0f84662db
7 changed files with 257 additions and 266 deletions
19
Meshtastic/Enums/MessageDestination.swift
Normal file
19
Meshtastic/Enums/MessageDestination.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
) {
|
||||
|
|
|
|||
115
Meshtastic/Views/Messages/MessageContextMenuItems.swift
Normal file
115
Meshtastic/Views/Messages/MessageContextMenuItems.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
89
Meshtastic/Views/Messages/MessageText.swift
Normal file
89
Meshtastic/Views/Messages/MessageText.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue