diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index c7c6385b..d2ae1e5a 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -93,12 +93,51 @@ 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() } + + // 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 = "" + let 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 && !currentScalar.isASCII { + // 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/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 2d86e15a..09af4418 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)") @@ -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/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/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 { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index abf8a0ec..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") } @@ -363,14 +363,14 @@ 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") } .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") } @@ -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)") } } } 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)