From d37572fde20ba4ef3bd56b29b58a1338090e6d9d Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Fri, 2 May 2025 16:54:16 -0700 Subject: [PATCH 1/2] Reply updates --- Localizable.xcstrings | 3 + .../Views/Messages/ChannelMessageList.swift | 45 +++++-- .../TextMessageField/TextMessageField.swift | 110 ++++++++++-------- .../Views/Messages/UserMessageList.swift | 44 +++++-- 4 files changed, 135 insertions(+), 67 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index a29691fb..834f3238 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -28749,6 +28749,9 @@ } } } + }, + "Replying to a message" : { + }, "Request Legacy Admin: %@" : { "localizations" : { diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 0696e9d8..59d68e9e 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -28,6 +28,8 @@ struct ChannelMessageList: View { @State private var hasReachedBottom = false @State private var gotFirstUnreadMessage: Bool = false + @State private var messageToHighlight: Int64 = 0 + var body: some View { VStack { ScrollViewReader { scrollView in @@ -39,16 +41,33 @@ struct ChannelMessageList: View { if message.replyID > 0 { let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID }) HStack { - Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2) - .padding(10) - .overlay( - RoundedRectangle(cornerRadius: 18) - .stroke(Color.blue, lineWidth: 0.5) - ) - Image(systemName: "arrowshape.turn.up.left.fill") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.accentColor) - .padding(.trailing) + Button { + if let messageNum = messageReply?.messageId { + withAnimation(.easeInOut(duration: 0.5)) { + messageToHighlight = messageNum + } + scrollView.scrollTo(messageNum, anchor: .center) + + // Reset highlight after delay + Task { + try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second + withAnimation(.easeInOut(duration: 0.5)) { + messageToHighlight = -1 + } + } + } + } label: { + Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2) + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.blue, lineWidth: 0.5) + ) + Image(systemName: "arrowshape.turn.up.left.fill") + .symbolRenderingMode(.hierarchical) + .imageScale(.large).foregroundColor(.accentColor) + .padding(.trailing) + } } } HStack(alignment: .bottom) { @@ -114,6 +133,12 @@ struct ChannelMessageList: View { Spacer(minLength: 50) } } + + .overlay { + RoundedRectangle(cornerRadius: 10) + .stroke(.blue, lineWidth: 2) + .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0) + } .padding([.leading, .trailing]) .frame(maxWidth: .infinity) .id(message.messageId) diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift index c8def69b..e6d7a15b 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -28,59 +28,75 @@ struct TextMessageField: View { #endif HStack(alignment: .top) { - ZStack { - TextField("message", text: $typingMessage, axis: .vertical) - .onChange(of: typingMessage) { _, value in - totalBytes = value.utf8.count - // Only mess with the value if it is too big - while totalBytes > Self.maxbytes { - typingMessage = String(typingMessage.dropLast()) - totalBytes = typingMessage.utf8.count - } - } - .keyboardType(.default) - .toolbar { - ToolbarItemGroup(placement: .keyboard) { - Button("Dismiss") { - isFocused = false + if replyMessageId != 0 { + + HStack { + Button { + withAnimation(.easeInOut(duration: 0.2)) { + replyMessageId = 0 } - .font(.subheadline) + isFocused = false + } label : { + Image(systemName: "x.circle.fill") + } + Text("Replying to a message") - if destination.showAlertButton { + } + } + ZStack { + TextField("message", text: $typingMessage, axis: .vertical) + .onChange(of: typingMessage) { _, value in + totalBytes = value.utf8.count + // Only mess with the value if it is too big + while totalBytes > Self.maxbytes { + typingMessage = String(typingMessage.dropLast()) + totalBytes = typingMessage.utf8.count + } + } + .keyboardType(.default) + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Button("Dismiss") { + isFocused = false + } + .font(.subheadline) + + if destination.showAlertButton { + Spacer() + AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" } + } + Spacer() - AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" } + RequestPositionButton(action: requestPosition) + TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes) } - - 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) + .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) + } } - .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) + .padding(.all, 15) + } private func requestPosition() { diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 6f995756..e1861240 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -26,6 +26,8 @@ struct UserMessageList: View { @State private var hasReachedBottom = false @State private var gotFirstUnreadMessage: Bool = false + @State private var messageToHighlight: Int64 = 0 + var body: some View { VStack { ScrollViewReader { scrollView in @@ -39,16 +41,33 @@ struct UserMessageList: View { if message.replyID > 0 { let messageReply = user.messageList.first(where: { $0.messageId == message.replyID }) HStack { - Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2) - .padding(10) - .overlay( - RoundedRectangle(cornerRadius: 18) - .stroke(Color.blue, lineWidth: 0.5) - ) - Image(systemName: "arrowshape.turn.up.left.fill") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.accentColor) - .padding(.trailing) + Button { + if let messageNum = messageReply?.messageId { + withAnimation(.easeInOut(duration: 0.5)) { + messageToHighlight = messageNum + } + scrollView.scrollTo(messageNum, anchor: .center) + + // Reset highlight after delay + Task { + try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second + withAnimation(.easeInOut(duration: 0.5)) { + messageToHighlight = -1 + } + } + } + } label: { + Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2) + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.blue, lineWidth: 0.5) + ) + Image(systemName: "arrowshape.turn.up.left.fill") + .symbolRenderingMode(.hierarchical) + .imageScale(.large).foregroundColor(.accentColor) + .padding(.trailing) + } } } HStack(alignment: .top) { @@ -101,6 +120,11 @@ struct UserMessageList: View { Spacer(minLength: 50) } } + .overlay { + RoundedRectangle(cornerRadius: 10) + .stroke(.blue, lineWidth: 2) + .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0) + } .padding([.leading, .trailing]) .frame(maxWidth: .infinity) .id(message.messageId) From 3290b35317928d6b2df66698f3419a288f2a9d8c Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 3 May 2025 23:50:10 -0700 Subject: [PATCH 2/2] Fixed conflict --- .../TextMessageField/TextMessageField.swift | 60 +++++++------------ 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift index a9e9c993..c642a9b7 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -15,54 +15,38 @@ struct TextMessageField: View { @State private var sendPositionWithMessage = false var body: some View { - #if targetEnvironment(macCatalyst) - HStack { - if destination.showAlertButton { + VStack { + #if targetEnvironment(macCatalyst) + HStack { + if destination.showAlertButton { + Spacer() + AlertButton { typingMessage += "🔔 Alert Bell! \u{7}" } + } Spacer() - AlertButton { typingMessage += "🔔 Alert Bell! \u{7}" } + RequestPositionButton(action: requestPosition) + TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing) } - Spacer() - RequestPositionButton(action: requestPosition) - TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing) - } - #endif + #endif - HStack(alignment: .top) { + HStack(alignment: .top) { if replyMessageId != 0 { - HStack { Button { withAnimation(.easeInOut(duration: 0.2)) { replyMessageId = 0 } isFocused = false - } label : { + } label: { Image(systemName: "x.circle.fill") } Text("Replying to a message") - } - } + } + ZStack { TextField("message", text: $typingMessage, axis: .vertical) .onChange(of: typingMessage) { _, value in totalBytes = value.utf8.count - // Only mess with the value if it is too big - while totalBytes > Self.maxbytes { - typingMessage = String(typingMessage.dropLast()) - totalBytes = typingMessage.utf8.count - } - } - } - Text("Replying to a message") - - } - } - ZStack { - TextField("message", text: $typingMessage, axis: .vertical) - .onChange(of: typingMessage) { _, value in - totalBytes = value.utf8.count - // Only mess with the value if it is too big while totalBytes > Self.maxbytes { typingMessage = String(typingMessage.dropLast()) totalBytes = typingMessage.utf8.count @@ -75,12 +59,12 @@ struct TextMessageField: View { 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) @@ -92,18 +76,18 @@ struct TextMessageField: View { .frame(minHeight: 50) .keyboardShortcut(.defaultAction) .onSubmit { -#if targetEnvironment(macCatalyst) + #if targetEnvironment(macCatalyst) sendMessage() -#endif + #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) @@ -111,13 +95,13 @@ struct TextMessageField: View { } } .padding(.all, 15) - + } } private func requestPosition() { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true - typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)." + typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)." } private func sendMessage() {