From 5188152f2f1d78aa573a5d907613bf8ea19e8948 Mon Sep 17 00:00:00 2001 From: Jake-B Date: Sat, 15 Mar 2025 09:47:52 -0400 Subject: [PATCH 1/4] Improvements to emoji handling for node names --- Meshtastic/Extensions/String.swift | 26 +++++++++++++++++++ Meshtastic/Views/Helpers/CircleText.swift | 2 +- .../Views/Helpers/ConnectedDevice.swift | 2 +- Meshtastic/Views/Settings/UserConfig.swift | 8 +++--- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index c7c6385b..aa28fa71 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -101,4 +101,30 @@ extension String { .map { String($0) } .joined() } + + // Adds variation selectors to prefer the graphical form of emoji. + // Looks ahead to make sure that the variation selector is not already applied. + var addingVariationSelectors: String { + var result = "" + var scalars = self.unicodeScalars + var index = scalars.startIndex + while index < scalars.endIndex { + let currentScalar = scalars[index] + result += String(currentScalar) + if currentScalar.properties.isEmoji && !currentScalar.properties.isEmojiPresentation { + // Check if the next scalar is U+FE0F + let nextIndex = scalars.index(after: index) + if nextIndex < scalars.endIndex && scalars[nextIndex].value == 0xFE0F { + // Already has variation selector; skip the next scalar + index = nextIndex + } else { + // Append variation selector + result += String(UnicodeScalar(0xFE0F)!) + } + } + // Move to the next scalar + index = scalars.index(after: index) + } + return result + } } diff --git a/Meshtastic/Views/Helpers/CircleText.swift b/Meshtastic/Views/Helpers/CircleText.swift index b7e4238d..b8f74842 100644 --- a/Meshtastic/Views/Helpers/CircleText.swift +++ b/Meshtastic/Views/Helpers/CircleText.swift @@ -16,7 +16,7 @@ struct CircleText: View { Circle() .fill(color) .frame(width: circleSize, height: circleSize) - Text(text) + Text(text.addingVariationSelectors) .frame(width: circleSize * 0.9, height: circleSize * 0.9, alignment: .center) .foregroundColor(color.isLight() ? .black : .white) .minimumScaleFactor(0.001) diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index 42b0ac70..c795b1b0 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -28,7 +28,7 @@ struct ConnectedDevice: View { .imageScale(.large) .foregroundColor(.green) .symbolRenderingMode(.hierarchical) - Text(name).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray) + Text(name.addingVariationSelectors).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray) } else { Image(systemName: "antenna.radiowaves.left.and.right.slash") .imageScale(.medium) diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index ea64e36e..644c0077 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -50,12 +50,14 @@ struct UserConfig: View { TextField("Long Name", text: $longName) .onChange(of: longName) { - var totalBytes = longName.utf8.count + var newValue = longName.withoutVariationSelectors + var totalBytes = newValue.utf8.count // Only mess with the value if it is too big while totalBytes > (isLicensed ? 6 : 36) { - longName = String(longName.dropLast()) - totalBytes = longName.utf8.count + newValue = String(newValue.dropLast()) + totalBytes = newValue.utf8.count } + longName = newValue } } .keyboardType(.default) From f730a93082354fa140dfe9a1dc7c518840ba6e95 Mon Sep 17 00:00:00 2001 From: Jake-B Date: Sat, 15 Mar 2025 10:13:19 -0400 Subject: [PATCH 2/4] Do not apply variation selectors to ASCII --- Meshtastic/Extensions/String.swift | 2 +- Meshtastic/Views/Nodes/Helpers/NodeListItem.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index aa28fa71..6dd3cad5 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -111,7 +111,7 @@ extension String { while index < scalars.endIndex { let currentScalar = scalars[index] result += String(currentScalar) - if currentScalar.properties.isEmoji && !currentScalar.properties.isEmojiPresentation { + if currentScalar.properties.isEmoji && !currentScalar.properties.isEmojiPresentation && !currentScalar.isASCII { // Check if the next scalar is U+FE0F let nextIndex = scalars.index(after: index) if nextIndex < scalars.endIndex && scalars[nextIndex].value == 0xFE0F { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 9819ef38..e57d99cc 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -64,7 +64,7 @@ struct NodeListItem: View { let (image, color) = userKeyStatus IconAndText(systemName: image, imageColor: color, - text: node.user?.longName ?? "unknown".localized, + text: node.user?.longName?.addingVariationSelectors ?? "unknown".localized, textColor: .primary) if node.favorite { Spacer() diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 12c76cd2..002fc695 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -264,7 +264,7 @@ struct NodeList: View { columnVisibility: columnVisibility ) .edgesIgnoringSafeArea([.leading, .trailing]) - .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) + .navigationBarTitle(String(node.user?.longName?.addingVariationSelectors ?? "unknown".localized), displayMode: .inline) .navigationBarItems( trailing: ZStack { if UIDevice.current.userInterfaceIdiom != .phone { From dcd061eece4f7b04ffc8c1230eb70267242135e4 Mon Sep 17 00:00:00 2001 From: Jake-B Date: Sat, 15 Mar 2025 11:15:51 -0400 Subject: [PATCH 3/4] Add variation selectors to longName in more spots --- Meshtastic/Views/Bluetooth/Connect.swift | 4 ++-- Meshtastic/Views/Settings/Settings.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 2d86e15a..6d804e3f 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -61,9 +61,9 @@ struct Connect: View { .padding(.trailing) VStack(alignment: .leading) { if node != nil { - Text(connectedPeripheral.longName).font(.title2) + Text(connectedPeripheral.longName.addingVariationSelectors).font(.title2) } - Text("BLE Name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name ?? "unknown".localized)") + Text("BLE Name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name?.addingVariationSelectors ?? "unknown".localized)") .font(.callout).foregroundColor(Color.gray) if node != nil { Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)") diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index abf8a0ec..8e24e500 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -363,7 +363,7 @@ struct Settings: View { .tag(Int(node.num)) } else if UserDefaults.enableAdministration && node.user?.pkiEncrypted ?? false { Label { - Text("Request PKI Admin: \(node.user?.longName ?? "unknown".localized)") + Text("Request PKI Admin: \(node.user?.longName?.addingVariationSelectors ?? "unknown".localized)") } icon: { Image(systemName: "rectangle.and.hand.point.up.left") } @@ -395,7 +395,7 @@ struct Settings: View { TipView(AdminChannelTip(), arrowEdge: .top) } else { if bleManager.connectedPeripheral != nil { - Text("Connected Node \(node?.user?.longName ?? "unknown".localized)") + Text("Connected Node \(node?.user?.longName?.addingVariationSelectors ?? "unknown".localized)") } } } From d06dd5e8bacb93371aa8ece2d72ebed1d3170f78 Mon Sep 17 00:00:00 2001 From: Jake-B Date: Sat, 15 Mar 2025 11:39:46 -0400 Subject: [PATCH 4/4] Better handling for numeric emojis --- Meshtastic/Extensions/String.swift | 23 ++++++++++++++++++----- Meshtastic/Views/Bluetooth/Connect.swift | 4 ++-- Meshtastic/Views/Settings/Settings.swift | 4 ++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index 6dd3cad5..d2ae1e5a 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -93,11 +93,24 @@ extension String { // Filter out variation selectors from the string var withoutVariationSelectors: String { - return self.unicodeScalars - .filter { scalar in - return !scalar.properties.isVariationSelector + var scalars: [UnicodeScalar] = [] + var previousWasASCII = false + + for scalar in self.unicodeScalars { + if scalar.properties.isVariationSelector { + // Only keep variation selector if the previous character was ASCII + if previousWasASCII { + scalars.append(scalar) + } + // No need to update previousWasASCII since variation selectors aren't characters + // Shouldn't have 2 in a row + } else { + scalars.append(scalar) + previousWasASCII = scalar.isASCII } - .compactMap { UnicodeScalar($0) } + } + + return scalars.compactMap { UnicodeScalar($0) } .map { String($0) } .joined() } @@ -106,7 +119,7 @@ extension String { // Looks ahead to make sure that the variation selector is not already applied. var addingVariationSelectors: String { var result = "" - var scalars = self.unicodeScalars + let scalars = self.unicodeScalars var index = scalars.startIndex while index < scalars.endIndex { let currentScalar = scalars[index] diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 6d804e3f..09af4418 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -120,7 +120,7 @@ struct Connect: View { #endif Text("Num: \(String(node!.num))") Text("Short Name: \(node?.user?.shortName ?? "?")") - Text("Long Name: \(node?.user?.longName ?? "unknown".localized)") + Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "unknown".localized)") Text("BLE RSSI: \(connectedPeripheral.rssi)") Button { @@ -333,7 +333,7 @@ struct Connect: View { let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 4")) let mostRecent = localStats?.lastObject as? TelemetryEntity - let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown") + let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName?.addingVariationSelectors ?? "unknown") let future = Date(timeIntervalSinceNow: Double(timerSeconds)) let initialContentState = MeshActivityAttributes.ContentState(uptimeSeconds: UInt32(mostRecent?.uptimeSeconds ?? 0), diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 8e24e500..8e0dcfc3 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -341,7 +341,7 @@ struct Settings: View { /// Connected Node if node.num == bleManager.connectedPeripheral?.num ?? 0 { Label { - Text("BLE: \(node.user?.longName ?? "unknown".localized)") + Text("BLE: \(node.user?.longName?.addingVariationSelectors ?? "unknown".localized)") } icon: { Image(systemName: "antenna.radiowaves.left.and.right") } @@ -370,7 +370,7 @@ struct Settings: View { .tag(Int(node.num)) } else if !UserDefaults.enableAdministration { Label { - Text("Request Legacy Admin: \(node.user?.longName ?? "unknown".localized)") + Text("Request Legacy Admin: \(node.user?.longName?.addingVariationSelectors ?? "unknown".localized)") } icon: { Image(systemName: "rectangle.and.hand.point.up.left") }