2.7.1 Working Changes (#1392)

* Bump version

* Offset for map controls on the mesh map

* Only mark messages as read if they are unread (#1388)

* Only mark messages as read if they are unread

* More cheap optimizations

* Fix map control positions on the route recorder

* Add seperate state variable for delete all channel messges button since the channelSelection is being used for navigation

* Use a seperate state variable to track what user messages are being deleted for as userSelection is being used for navigation

* Get the ringtone if external notifications is enabled

* Fix RTTTL typo

* Dont show modem lights popover if we are on macOS 26 cause it crashes

* Fix annoying connect bottom background bug

* Update mesh map detents

* Move divider inside of the hstack keyboard toolbar

* Update Meshtastic/Helpers/MeshPackets.swift

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Remove extra environment variable that is not getting used

---------

Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Garth Vander Houwen 2025-09-12 21:45:05 -07:00 committed by GitHub
parent e5bc161444
commit 88ed168725
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 87 additions and 28 deletions

View file

@ -2043,7 +2043,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.7.0;
MARKETING_VERSION = 2.7.1;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -2076,7 +2076,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.7.0;
MARKETING_VERSION = 2.7.1;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -2107,7 +2107,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.7.0;
MARKETING_VERSION = 2.7.1;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -2139,7 +2139,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.7.0;
MARKETING_VERSION = 2.7.1;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -164,6 +164,10 @@ extension AccessoryManager {
if moduleConfigPacket.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfigPacket.cannedMessage) {
try? getCannedMessageModuleMessages(destNum: deviceNum, wantResponse: true)
}
// Get the Ringtone if the Module is External Notifications
if moduleConfigPacket.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(moduleConfigPacket.externalNotification) {
try? getRingtone(destNum: deviceNum, wantResponse: true)
}
}
func handleDeviceMetadata(_ metadata: DeviceMetadata) {

View file

@ -47,6 +47,43 @@ extension AccessoryManager {
try await send(toRadio, debugDescription: logString)
}
}
public func getRingtone(destNum: Int64, wantResponse: Bool) throws {
guard let deviceNum = self.activeConnection?.device.num else {
Logger.services.error("Error while sending RtttlConfig request. No active device.")
throw AccessoryError.ioFailed("No active device")
}
var adminPacket = AdminMessage()
adminPacket.getRingtoneRequest = true
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(destNum)
meshPacket.from = UInt32(deviceNum)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
meshPacket.decoded.wantResponse = wantResponse
var dataMessage = DataMessage()
guard let adminData: Data = try? adminPacket.serializedData() else {
throw AccessoryError.ioFailed("Error serializing admin packet")
}
dataMessage.payload = adminData
dataMessage.portnum = PortNum.adminApp
dataMessage.wantResponse = wantResponse
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let logString = String.localizedStringWithFormat("Requested RTTTL Config Module ringtone for node: %@".localized, String(deviceNum))
Task {
try await send(toRadio, debugDescription: logString)
}
}
public func saveTimeZone(config: Config.DeviceConfig, user: Int64) async throws -> Int64 {
var adminPacket = AdminMessage()

View file

@ -585,8 +585,9 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
upsertTelemetryModuleConfigPacket(config: moduleConfig.telemetry, nodeNum: Int64(packet.from), context: context)
}
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getRingtoneResponse(adminMessage.getRingtoneResponse) {
let ringtone = adminMessage.getRingtoneResponse
upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: Int64(packet.from), context: context)
if let rt = try? RTTTLConfig(serializedBytes: packet.decoded.payload) {
upsertRtttlConfigPacket(ringtone: rt.ringtone, nodeNum: Int64(packet.from), context: context)
}
} else {
Logger.mesh.error("🕸️ MESH PACKET received Admin App UNHANDLED \((try? packet.decoded.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
}

View file

@ -314,7 +314,7 @@ struct Connect: View {
.textCase(nil)
}
}
.scrollContentBackground(.hidden)
HStack(alignment: .center) {
Spacer()
#if targetEnvironment(macCatalyst)
@ -338,8 +338,9 @@ struct Connect: View {
Spacer()
}
.padding(.bottom, 10)
.background(Color(.tertiarySystemGroupedBackground))
}
.background(Color(.systemGroupedBackground))
.navigationTitle("Connect")
.navigationBarItems(
leading: MeshtasticLogo(),

View file

@ -29,7 +29,16 @@ struct RXTXIndicatorWidget: View {
}
}
}
self.isPopoverOpen.toggle()
#if targetEnvironment(macCatalyst)
if #available(macOS 26.0, *) {
// Dont show popover that crashes on mac 26
} else {
self.isPopoverOpen.toggle()
}
#else
self.isPopoverOpen.toggle()
#endif
}) {
VStack(spacing: 3.0) {
HStack(spacing: 2.0) {

View file

@ -13,13 +13,9 @@ struct ChannelList: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var accessoryManager: AccessoryManager
@Binding
var node: NodeInfoEntity?
@Binding
var channelSelection: ChannelEntity?
@Binding var node: NodeInfoEntity?
@Binding var channelSelection: ChannelEntity?
@State private var channelToDeleteMessages: ChannelEntity?
@State private var isPresentingDeleteChannelMessagesConfirm: Bool = false
@State private var isPresentingTraceRouteSentAlert = false
@State private var showingHelp = false
@ -123,7 +119,7 @@ struct ChannelList: View {
if channel.allPrivateMessages.count > 0 {
Button(role: .destructive) {
isPresentingDeleteChannelMessagesConfirm = true
channelSelection = channel
channelToDeleteMessages = channel
} label: {
Label("Delete Messages", systemImage: "trash")
}
@ -158,9 +154,9 @@ struct ChannelList: View {
titleVisibility: .visible
) {
Button(role: .destructive) {
deleteChannelMessages(channel: channelSelection!, context: context)
deleteChannelMessages(channel: channelToDeleteMessages!, context: context)
context.refresh(myInfo, mergeChanges: true)
channelSelection = nil
channelToDeleteMessages = nil
} label: {
Text("Delete")
}

View file

@ -130,7 +130,6 @@ struct ChannelMessageList: View {
TapbackResponses(message: message) {
appState.unreadChannelMessages = myInfo.unreadMessages
context.refresh(myInfo, mergeChanges: true)
}
HStack {
@ -159,7 +158,9 @@ struct ChannelMessageList: View {
.frame(maxWidth: .infinity)
.id(message.messageId)
.onAppear {
markMessagesAsRead()
if !message.read {
markMessagesAsRead()
}
}
}
Color.clear

View file

@ -68,8 +68,8 @@ struct TextMessageField: View {
}
}
.padding(15)
Divider()
if isFocused {
Divider()
HStack {
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }

View file

@ -18,10 +18,9 @@ struct UserList: View {
@State private var showingHelp = false
@State private var showingTrustConfirm: Bool = false
@StateObject private var filters: NodeFilterParameters = NodeFilterParameters()
@Binding var node: NodeInfoEntity?
@Binding var userSelection: UserEntity?
@State private var userToDeleteMessages: UserEntity?
@State private var isPresentingDeleteUserMessagesConfirm: Bool = false
private func fetchUsers(withFilters: NodeFilterParameters) -> [UserEntity] {
@ -152,7 +151,7 @@ struct UserList: View {
if user.messageList.count > 0 {
Button(role: .destructive) {
isPresentingDeleteUserMessagesConfirm = true
userSelection = user
userToDeleteMessages = user
} label: {
Label("Delete Messages", systemImage: "trash")
}
@ -164,7 +163,7 @@ struct UserList: View {
titleVisibility: .visible
) {
Button(role: .destructive) {
deleteUserMessages(user: userSelection!, context: context)
deleteUserMessages(user: userToDeleteMessages!, context: context)
context.refresh(node!.user!, mergeChanges: true)
} label: {
Text("Delete")

View file

@ -74,6 +74,7 @@ struct MeshMap: View {
.mapControlVisibility(.automatic)
}
.controlSize(.regular)
.offset(y: 100)
.onMapCameraChange(frequency: MapCameraUpdateFrequency.continuous, { context in
distance = context.camera.distance
})
@ -126,7 +127,7 @@ struct MeshMap: View {
}
.sheet(isPresented: $editingSettings) {
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs)
.presentationDetents([.fraction(0.85), .large])
.presentationDetents([.large])
}
.onChange(of: router.navigationState) {

View file

@ -49,6 +49,16 @@ struct RouteRecorder: View {
}
.mapStyle(mapStyle)
}
.mapControls {
MapScaleView(scope: routerecorderscope)
.mapControlVisibility(.automatic)
MapPitchToggle(scope: routerecorderscope)
.mapControlVisibility(.automatic)
MapCompass(scope: routerecorderscope)
.mapControlVisibility(.automatic)
}
.controlSize(.regular)
.offset(y: 100)
.mapScope(routerecorderscope)
.safeAreaInset(edge: .bottom) {
ZStack {

View file

@ -236,7 +236,7 @@ struct Settings: View {
}
}
if isModuleSupported(.audioConfig) {
if isModuleSupported(.extnotifConfig) {
NavigationLink(value: SettingsNavigationState.ringtone) {
Label {
Text("Ringtone")