diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 95f524d2..de4df376 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -27785,6 +27785,9 @@ } } } + }, + "Replying to a message" : { + }, "Request Legacy Admin: %@" : { "localizations" : { diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index e400b5fe..6093d9eb 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -25,6 +25,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 @@ -36,16 +38,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) { @@ -111,6 +130,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 180ec7cd..c642a9b7 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -15,78 +15,93 @@ 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) { - 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 + HStack(alignment: .top) { + 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") + } + } + + ZStack { + TextField("message", text: $typingMessage, axis: .vertical) + .onChange(of: typingMessage) { _, value in + totalBytes = value.utf8.count + 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}" } + } - if destination.showAlertButton { 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 - } + .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) + 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) + Button(action: sendMessage) { + Image(systemName: "arrow.up.circle.fill") + .font(.largeTitle) + .foregroundColor(.accentColor) + } } + .padding(.all, 15) } - .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() { diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 9824f621..686decaf 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -25,6 +25,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 @@ -38,16 +40,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) { @@ -100,6 +119,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)