improvement: add MessageText views

This commit is contained in:
Austin Payne 2024-02-17 13:26:09 -07:00
parent 5134e1d65d
commit d0f84662db
7 changed files with 257 additions and 266 deletions

View file

@ -14,6 +14,9 @@
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 */; };
D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */; };
D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; };
D93068D72B8146690066FBC8 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D62B8146690066FBC8 /* MessageText.swift */; };
D9BC22DB2B7DE8E2006A37D5 /* TileDownloadStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BC22DA2B7DE8E2006A37D5 /* TileDownloadStatus.swift */; };
D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */; };
D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */; };
@ -235,6 +238,9 @@
B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; 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>"; };
D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = "<group>"; };
D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = "<group>"; };
D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = "<group>"; };
D9BC22DA2B7DE8E2006A37D5 /* TileDownloadStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileDownloadStatus.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>"; };
@ -693,6 +699,7 @@
DD1925B828CDA93900720036 /* SerialConfigEnums.swift */,
DD994B68295F88B60013760A /* IntervalEnums.swift */,
DD5E5239298EFA5300D21B61 /* TelemetryWeather.swift */,
D93068D42B812B700066FBC8 /* MessageDestination.swift */,
);
path = Enums;
sourceTree = "<group>";
@ -850,6 +857,8 @@
DDB8F4112A9EE5DD00230ECE /* UserList.swift */,
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */,
B399E8A32B6F486400E4488E /* RetryButton.swift */,
D93068D62B8146690066FBC8 /* MessageText.swift */,
D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */,
);
path = Messages;
sourceTree = "<group>";
@ -1242,6 +1251,7 @@
DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */,
DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */,
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */,
D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */,
DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */,
DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */,
DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */,
@ -1298,6 +1308,7 @@
DD5E523A298EFA5300D21B61 /* TelemetryWeather.swift in Sources */,
DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */,
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */,
D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */,
DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */,
DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */,
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */,
@ -1316,6 +1327,7 @@
DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */,
B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */,
DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */,
D93068D72B8146690066FBC8 /* MessageText.swift in Sources */,
DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */,
DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */,
DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */,

View 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
}
}
}

View file

@ -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
) {

View 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
}
}
}

View 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
}
}
}

View file

@ -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)
}
}

View file

@ -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
) {