diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 8d5bd14d..99382d5b 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -53,160 +53,155 @@ struct ChannelMessageList: View { } var body: some View { - NavigationStack { - ScrollViewReader { scrollView in - ScrollView { - LazyVStack { - ForEach(allPrivateMessages) { message in - let thisMessageIndex = allPrivateMessages.firstIndex(of: message) ?? 0 - let previousMessage = thisMessageIndex > 0 ? allPrivateMessages[thisMessageIndex - 1] : nil - let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false) - if message.displayTimestamp(aboveMessage: previousMessage) { - Text(message.timestamp.formatted(date: .abbreviated, time: .shortened)) - .font(.caption) - .foregroundColor(.gray) - } - if message.replyID > 0 { - let messageReply = allPrivateMessages.first(where: { $0.messageId == message.replyID }) - HStack { - Button { - if let messageNum = messageReply?.messageId { + ScrollViewReader { scrollView in + ScrollView { + LazyVStack { + ForEach(allPrivateMessages) { message in + let thisMessageIndex = allPrivateMessages.firstIndex(of: message) ?? 0 + let previousMessage = thisMessageIndex > 0 ? allPrivateMessages[thisMessageIndex - 1] : nil + let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false) + if message.displayTimestamp(aboveMessage: previousMessage) { + Text(message.timestamp.formatted(date: .abbreviated, time: .shortened)) + .font(.caption) + .foregroundColor(.gray) + } + if message.replyID > 0 { + let messageReply = allPrivateMessages.first(where: { $0.messageId == message.replyID }) + HStack { + Button { + if let messageNum = messageReply?.messageId { + withAnimation(.easeInOut(duration: 0.5)) { + messageToHighlight = messageNum + } + scrollView.scrollTo(messageNum, anchor: .center) + Task { + try? await Task.sleep(nanoseconds: 1_000_000_000) withAnimation(.easeInOut(duration: 0.5)) { - messageToHighlight = messageNum - } - scrollView.scrollTo(messageNum, anchor: .center) - Task { - try? await Task.sleep(nanoseconds: 1_000_000_000) - withAnimation(.easeInOut(duration: 0.5)) { - messageToHighlight = -1 - } + 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) } + } 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) { - if currentUser { Spacer(minLength: 50) } - if !currentUser { - CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44, node: getNodeInfo(id: Int64(message.fromUser?.num ?? 0), context: context)) - .padding(.all, 5) - .offset(y: -7) - } - - VStack(alignment: currentUser ? .trailing : .leading) { - let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) - - if !currentUser && message.fromUser != nil { - Text("\(message.fromUser?.longName ?? "Unknown".localized ) (\(message.fromUser?.userId ?? "?"))") - .font(.caption) - .foregroundColor(.gray) - .offset(y: 8) - } - - HStack { - MessageText( - message: message, - tapBackDestination: .channel(channel), - isCurrentUser: currentUser - ) { - self.replyMessageId = message.messageId - self.messageFieldFocused = true - } - - if currentUser && message.canRetry { - RetryButton(message: message, destination: .channel(channel)) - } - } - - TapbackResponses(message: message) { - appState.unreadChannelMessages = myInfo.unreadMessages - context.refresh(myInfo, mergeChanges: true) - } - - HStack { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) - if currentUser && message.receivedACK { - Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - .foregroundStyle(ackErrorVal?.color ?? Color.red) - .font(.caption2) - } else if currentUser && message.ackError == 0 { - Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange) - } else if currentUser && !isDetectionSensorMessage { - Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - .foregroundStyle(ackErrorVal?.color ?? Color.red) - .font(.caption2) - } - } - } - .padding(.bottom) - .id(allPrivateMessages.firstIndex(of: message)) - - if !currentUser { - Spacer(minLength: 50) - } - } - .padding([.leading, .trailing]) - .frame(maxWidth: .infinity) - .id(message.messageId) - .onAppear { - markMessagesAsRead() - } } - Color.clear - .frame(height: 1) - .id("bottomAnchor") - } - } - .defaultScrollAnchor(.bottom) - .defaultScrollAnchorTopAlignment() - .defaultScrollAnchorBottomSizeChanges() - .scrollDismissesKeyboard(.immediately) - .onChange(of: messageFieldFocused) { - if messageFieldFocused { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - scrollView.scrollTo("bottomAnchor", anchor: .bottom) + HStack(alignment: .bottom) { + if currentUser { Spacer(minLength: 50) } + if !currentUser { + CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44, node: getNodeInfo(id: Int64(message.fromUser?.num ?? 0), context: context)) + .padding(.all, 5) + .offset(y: -7) + } + + VStack(alignment: currentUser ? .trailing : .leading) { + let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) + + if !currentUser && message.fromUser != nil { + Text("\(message.fromUser?.longName ?? "Unknown".localized ) (\(message.fromUser?.userId ?? "?"))") + .font(.caption) + .foregroundColor(.gray) + .offset(y: 8) + } + + HStack { + MessageText( + message: message, + tapBackDestination: .channel(channel), + isCurrentUser: currentUser + ) { + self.replyMessageId = message.messageId + self.messageFieldFocused = true + } + + if currentUser && message.canRetry { + RetryButton(message: message, destination: .channel(channel)) + } + } + + TapbackResponses(message: message) { + appState.unreadChannelMessages = myInfo.unreadMessages + context.refresh(myInfo, mergeChanges: true) + } + + HStack { + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + if currentUser && message.receivedACK { + Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) + .foregroundStyle(ackErrorVal?.color ?? Color.red) + .font(.caption2) + } else if currentUser && message.ackError == 0 { + Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange) + } else if currentUser && !isDetectionSensorMessage { + Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) + .foregroundStyle(ackErrorVal?.color ?? Color.red) + .font(.caption2) + } + } + } + .padding(.bottom) + .id(allPrivateMessages.firstIndex(of: message)) + + if !currentUser { + Spacer(minLength: 50) + } + } + .padding([.leading, .trailing]) + .frame(maxWidth: .infinity) + .id(message.messageId) + .onAppear { + markMessagesAsRead() } } - } - .safeAreaInset(edge: .bottom) { - TextMessageField( - destination: .channel(channel), - replyMessageId: $replyMessageId, - isFocused: $messageFieldFocused - ) - .background(.bar) + Color.clear + .frame(height: 1) + .id("bottomAnchor") } } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - HStack { - CircleText(text: String(channel.index), color: .accentColor, circleSize: 44).fixedSize() - Text(String(channel.name ?? "Unknown").camelCaseToWords()).font(.headline) + .defaultScrollAnchor(.bottom) + .defaultScrollAnchorTopAlignment() + .defaultScrollAnchorBottomSizeChanges() + .scrollDismissesKeyboard(.immediately) + .onChange(of: messageFieldFocused) { + if messageFieldFocused { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + scrollView.scrollTo("bottomAnchor", anchor: .bottom) } } - ToolbarItem(placement: .navigationBarTrailing) { - ZStack { - ConnectedDevice( - deviceConnected: accessoryManager.isConnected, - name: accessoryManager.activeConnection?.device.shortName ?? "?", - mqttProxyConnected: accessoryManager.mqttProxyConnected && (channel.uplinkEnabled || channel.downlinkEnabled), - mqttUplinkEnabled: channel.uplinkEnabled, - mqttDownlinkEnabled: channel.downlinkEnabled, - mqttTopic: accessoryManager.mqttManager.topic - ) - } + } + TextMessageField( + destination: .channel(channel), + replyMessageId: $replyMessageId, + isFocused: $messageFieldFocused + ) + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + HStack { + CircleText(text: String(channel.index), color: .accentColor, circleSize: 44).fixedSize() + Text(String(channel.name ?? "Unknown").camelCaseToWords()).font(.headline) + } + } + ToolbarItem(placement: .navigationBarTrailing) { + ZStack { + ConnectedDevice( + deviceConnected: accessoryManager.isConnected, + name: accessoryManager.activeConnection?.device.shortName ?? "?", + mqttProxyConnected: accessoryManager.mqttProxyConnected && (channel.uplinkEnabled || channel.downlinkEnabled), + mqttUplinkEnabled: channel.uplinkEnabled, + mqttDownlinkEnabled: channel.downlinkEnabled, + mqttTopic: accessoryManager.mqttManager.topic + ) } } } diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift index c7371851..cc0f9b84 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -5,6 +5,8 @@ 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 @@ -16,19 +18,7 @@ struct TextMessageField: View { var body: some View { SessionReplayPrivacyView(textAndInputPrivacy: .maskAllInputs) { - VStack { -#if targetEnvironment(macCatalyst) - HStack { - if destination.showAlertButton { - Spacer() - AlertButton { typingMessage += "🔔 Alert Bell! \u{7}" } - } - Spacer() - RequestPositionButton(action: requestPosition) - TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing) - } -#endif - + VStack(spacing: 0) { HStack(alignment: .top) { if replyMessageId != 0 { HStack { @@ -44,7 +34,7 @@ struct TextMessageField: View { } .padding(.top) } - + ZStack { TextField("Message", text: $typingMessage, axis: .vertical) .onChange(of: typingMessage) { _, value in @@ -55,40 +45,22 @@ struct TextMessageField: View { } } .keyboardType(.default) - .toolbar { - ToolbarItemGroup(placement: .keyboard) { - Button("Dismiss") { - 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) - } - } + // Remove toolbar here .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") @@ -96,7 +68,26 @@ struct TextMessageField: View { .foregroundColor(.accentColor) } } - .padding(.all, 15) + .padding(.horizontal, 15) + .padding(.top, 15) + .padding(.bottom, 10) + Divider() + if isFocused { + HStack { + Button("Dismiss") { + isFocused = false + } + 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) + } } } } @@ -117,7 +108,6 @@ struct TextMessageField: View { isEmoji: false, replyID: replyMessageId) - // If nothing thrown, then successful. Reset for the next message typingMessage = "" isFocused = false replyMessageId = 0 @@ -128,7 +118,6 @@ struct TextMessageField: View { destNum: destination.positionDestNum, wantResponse: destination.wantPositionResponse ) - // If nothing thrown, then successful. Logger.mesh.info("Location Sent") } } catch { @@ -153,13 +142,6 @@ private extension MessageDestination { } } - var showAlertButton: Bool { - switch self { - case .user: return true - case .channel: return true - } - } - var wantPositionResponse: Bool { switch self { case .user: return true