diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt index 81d7d9b97..fa3ef467c 100644 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt @@ -30,6 +30,8 @@ import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs import org.meshtastic.core.prefs.emoji.CustomEmojiPrefsImpl import org.meshtastic.core.prefs.filter.FilterPrefs import org.meshtastic.core.prefs.filter.FilterPrefsImpl +import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs +import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefsImpl import org.meshtastic.core.prefs.map.MapConsentPrefs import org.meshtastic.core.prefs.map.MapConsentPrefsImpl import org.meshtastic.core.prefs.map.MapPrefs @@ -54,6 +56,10 @@ import javax.inject.Singleton @Retention(AnnotationRetention.BINARY) internal annotation class AnalyticsSharedPreferences +@Qualifier +@Retention(AnnotationRetention.BINARY) +internal annotation class HomoglyphEncodingSharedPreferences + @Qualifier @Retention(AnnotationRetention.BINARY) internal annotation class AppSharedPreferences @@ -101,6 +107,8 @@ interface PrefsModule { @Binds fun bindAnalyticsPrefs(analyticsPrefsImpl: AnalyticsPrefsImpl): AnalyticsPrefs + @Binds fun bindHomoglyphEncodingPrefs(homoglyphEncodingPrefsImpl: HomoglyphPrefsImpl): HomoglyphPrefs + @Binds fun bindCustomEmojiPrefs(customEmojiPrefsImpl: CustomEmojiPrefsImpl): CustomEmojiPrefs @Binds fun bindMapConsentPrefs(mapConsentPrefsImpl: MapConsentPrefsImpl): MapConsentPrefs @@ -127,6 +135,12 @@ interface PrefsModule { fun provideAnalyticsSharedPreferences(@ApplicationContext context: Context): SharedPreferences = context.getSharedPreferences("analytics-prefs", Context.MODE_PRIVATE) + @Provides + @Singleton + @HomoglyphEncodingSharedPreferences + fun provideHomoglyphEncodingSharedPreferences(@ApplicationContext context: Context): SharedPreferences = + context.getSharedPreferences("homoglyph-encoding-prefs", Context.MODE_PRIVATE) + @Provides @Singleton @AppSharedPreferences diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt new file mode 100644 index 000000000..d74962cfe --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.homoglyph + +import android.content.SharedPreferences +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import org.meshtastic.core.prefs.PrefDelegate +import org.meshtastic.core.prefs.di.HomoglyphEncodingSharedPreferences +import javax.inject.Inject +import javax.inject.Singleton + +interface HomoglyphPrefs { + + /** Preference for whether homoglyph encoding is enabled by the user. */ + var homoglyphEncodingEnabled: Boolean + + /** + * Provides a [Flow] that emits the current state of [homoglyphEncodingEnabled] and subsequent changes. + * + * @return A [Flow] of [Boolean] indicating if homoglyph encoding is enabled. + */ + fun getHomoglyphEncodingEnabledChangesFlow(): Flow + + companion object { + /** Key for the homoglyphEncodingEnabled preference. */ + const val KEY_HOMOGLYPH_ENCODING_ENABLED = "enabled" + } +} + +@Singleton +class HomoglyphPrefsImpl +@Inject +constructor( + @HomoglyphEncodingSharedPreferences private val homoglyphEncodingSharedPreferences: SharedPreferences, +) : HomoglyphPrefs { + override var homoglyphEncodingEnabled: Boolean by + PrefDelegate(homoglyphEncodingSharedPreferences, HomoglyphPrefs.KEY_HOMOGLYPH_ENCODING_ENABLED, false) + + override fun getHomoglyphEncodingEnabledChangesFlow(): Flow = callbackFlow { + val listener = + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == HomoglyphPrefs.KEY_HOMOGLYPH_ENCODING_ENABLED) { + trySend(homoglyphEncodingEnabled) + } + } + // Emit the initial value + trySend(homoglyphEncodingEnabled) + homoglyphEncodingSharedPreferences.registerOnSharedPreferenceChangeListener(listener) + awaitClose { homoglyphEncodingSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) } + } +} diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml index ae910128f..202ec96b4 100644 --- a/core/strings/src/commonMain/composeResources/values/strings.xml +++ b/core/strings/src/commonMain/composeResources/values/strings.xml @@ -294,6 +294,7 @@ System default Choose theme Provide phone location to mesh + Compact encoding for Cyrillic Delete message? Delete %1$s messages? diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index 268ef7c42..21a483faa 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -62,4 +62,6 @@ dependencies { androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.test.ext.junit) + + testImplementation(libs.junit) } diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterStringTransformer.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterStringTransformer.kt new file mode 100644 index 000000000..9aebea8a0 --- /dev/null +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterStringTransformer.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.messaging + +/** + * This util class allows you to optimize the binary size of the transmitted text message strings. It replaces certain + * characters from national alphabets with the characters from the latin alphabet that have an identical appearance + * (homoglyphs), for example: cyrillic "А", "С", "у" -> latin "A", "C", "y", etc. According to statistics, such letters + * can make up about 20-25% of the total number of letters in the average text. Replacing them with Latin characters + * reduces the binary size of the transmitted message. The average transmitted message volume can then fit around + * ~140-145 characters instead of ~115-120 + */ +internal object HomoglyphCharacterStringTransformer { + + /** + * Unicode characters from the basic cyrillic block (U+0400-U+04FF), each of which occupies 2 bytes + * https://www.compart.com/en/unicode/block/U+0400 Mapped with the corresponding similarly written latin characters, + * each of which occupies 1 byte + * + * Please note that only 100% "reliable", completely visually identical characters are presented will here The + * characters that look like latin but contain various descenders, hooks, strokes, etc are not replaced with + * "simplified" latin appearance and will remain 2 byte unicode, as usual + */ + private val homoglyphCharactersSubstitutionMapping: Map = + mapOf( + '\u0405' to 'S', // https://www.compart.com/en/unicode/U+0405 - Cyrillic Capital Letter Dze + '\u0406' to + 'I', // https://www.compart.com/en/unicode/U+0406 - Cyrillic Capital Letter Byelorussian-Ukrainian I + '\u0408' to 'J', // https://www.compart.com/en/unicode/U+0408 - Cyrillic Capital Letter Je + '\u0410' to 'A', // https://www.compart.com/en/unicode/U+0410 - Cyrillic Capital Letter A + '\u0412' to 'B', // https://www.compart.com/en/unicode/U+0412 - Cyrillic Capital Letter Ve + '\u0415' to 'E', // https://www.compart.com/en/unicode/U+0415 - Cyrillic Capital Letter Ie + '\u041A' to 'K', // https://www.compart.com/en/unicode/U+041A - Cyrillic Capital Letter Ka + '\u041C' to 'M', // https://www.compart.com/en/unicode/U+041C - Cyrillic Capital Letter Em + '\u041D' to 'H', // https://www.compart.com/en/unicode/U+041D - Cyrillic Capital Letter En + '\u041E' to 'O', // https://www.compart.com/en/unicode/U+041E - Cyrillic Capital Letter O + '\u0420' to 'P', // https://www.compart.com/en/unicode/U+0420 - Cyrillic Capital Letter Er + '\u0421' to 'C', // https://www.compart.com/en/unicode/U+0421 - Cyrillic Capital Letter Es + '\u0422' to 'T', // https://www.compart.com/en/unicode/U+0422 - Cyrillic Capital Letter Te + '\u0425' to 'X', // https://www.compart.com/en/unicode/U+0425 - Cyrillic Capital Letter Ha + '\u0430' to 'a', // https://www.compart.com/en/unicode/U+0430 - Cyrillic Small Letter A + '\u0435' to 'e', // https://www.compart.com/en/unicode/U+0435 - Cyrillic Small Letter Ie + '\u043E' to 'o', // https://www.compart.com/en/unicode/U+043E - Cyrillic Small Letter O + '\u0440' to 'p', // https://www.compart.com/en/unicode/U+0440 - Cyrillic Small Letter Er + '\u0441' to 'c', // https://www.compart.com/en/unicode/U+0441 - Cyrillic Small Letter Es + '\u0443' to 'y', // https://www.compart.com/en/unicode/U+0443 - Cyrillic Small Letter U + '\u0445' to 'x', // https://www.compart.com/en/unicode/U+0445 - Cyrillic Small Letter Ha + '\u0455' to 's', // https://www.compart.com/en/unicode/U+0455 - Cyrillic Small Letter Dze + '\u0456' to + 'i', // https://www.compart.com/en/unicode/U+0456 - Cyrillic Small Letter Byelorussian-Ukrainian I + '\u0458' to 'j', // https://www.compart.com/en/unicode/U+0458 - Cyrillic Small Letter Je + '\u04AE' to 'Y', // https://www.compart.com/en/unicode/U+04AE - Cyrillic Capital Letter Straight U + '\u0417' to '3', // https://www.compart.com/en/unicode/U+0417 - Cyrillic Capital Letter Ze + // Note that capital "ze" here is a bit special - it technically transforms to a digit "three" + // The visuals are all the same, across the different fonts etc& The core idea is the same: + // We are still replacing 2-byte unicode letter with a digit character that occupies 1 byte in Unicode + // But I have to point it out to avoid confusion + + ) + + /** + * Returns the transformed optimized [String] value, in which some characters of the national alphabets are replaced + * with identical Latin characters so that the text takes up fewer bytes and is more compact for transmission. + * + * @param value original string value. + * @return optimized string value. + */ + fun optimizeUtf8StringWithHomoglyphs(value: String): String { + val stringBuilder = StringBuilder() + for (c in value.toCharArray()) stringBuilder.append(homoglyphCharactersSubstitutionMapping.getOrDefault(c, c)) + return stringBuilder.toString() + } +} diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt index 996587b5a..10a514317 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt @@ -178,6 +178,7 @@ fun MessageScreen( val quickChatActions by viewModel.quickChatActions.collectAsStateWithLifecycle(initialValue = emptyList()) val pagedMessages = viewModel.getMessagesFromPaged(contactKey).collectAsLazyPagingItems() val contactSettings by viewModel.contactSettings.collectAsStateWithLifecycle(initialValue = emptyMap()) + val homoglyphEncodingEnabled by viewModel.homoglyphEncodingEnabled.collectAsStateWithLifecycle(initialValue = false) // UI State managed within this Composable var replyingToPacketId by rememberSaveable { mutableStateOf(null) } @@ -469,6 +470,7 @@ fun MessageScreen( ) MessageInput( isEnabled = connectionState.isConnected(), + isHomoglyphEncodingEnabled = homoglyphEncodingEnabled, textFieldState = messageInputState, onSendMessage = { val messageText = messageInputState.text.toString().trim() @@ -938,12 +940,21 @@ private const val MAX_LINES = 3 @Composable private fun MessageInput( isEnabled: Boolean, + isHomoglyphEncodingEnabled: Boolean, textFieldState: TextFieldState, modifier: Modifier = Modifier, maxByteSize: Int = MESSAGE_CHARACTER_LIMIT_BYTES, onSendMessage: () -> Unit, ) { - val currentText = textFieldState.text.toString() + val currentTextRaw = textFieldState.text.toString() + + val currentText = + if (isHomoglyphEncodingEnabled) { + HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(currentTextRaw) + } else { + currentTextRaw + } + val currentByteLength = remember(currentText) { // Recalculate only when text changes @@ -1000,12 +1011,23 @@ private fun MessageInputPreview() { AppTheme { Surface { Column(modifier = Modifier.padding(8.dp)) { - MessageInput(isEnabled = true, textFieldState = rememberTextFieldState("Hello"), onSendMessage = {}) + MessageInput( + isEnabled = true, + isHomoglyphEncodingEnabled = false, + textFieldState = rememberTextFieldState("Hello"), + onSendMessage = {}, + ) Spacer(Modifier.size(16.dp)) - MessageInput(isEnabled = false, textFieldState = rememberTextFieldState("Disabled"), onSendMessage = {}) + MessageInput( + isEnabled = false, + isHomoglyphEncodingEnabled = false, + textFieldState = rememberTextFieldState("Disabled"), + onSendMessage = {}, + ) Spacer(Modifier.size(16.dp)) MessageInput( isEnabled = true, + isHomoglyphEncodingEnabled = false, textFieldState = rememberTextFieldState( "A very long message that might exceed the byte limit " + @@ -1018,6 +1040,7 @@ private fun MessageInputPreview() { // Test Japanese characters (multi-byte) MessageInput( isEnabled = true, + isHomoglyphEncodingEnabled = false, textFieldState = rememberTextFieldState("こんにちは世界"), // Hello World in Japanese onSendMessage = {}, maxByteSize = 10, diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index c58a64ca2..260b36ad0 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -44,6 +44,7 @@ import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.Capabilities import org.meshtastic.core.model.DataPacket import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs +import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.service.MeshServiceNotifications import org.meshtastic.core.service.ServiceAction @@ -67,6 +68,7 @@ constructor( private val packetRepository: PacketRepository, private val uiPrefs: UiPrefs, private val customEmojiPrefs: CustomEmojiPrefs, + private val homoglyphEncodingPrefs: HomoglyphPrefs, private val meshServiceNotifications: MeshServiceNotifications, ) : ViewModel() { private val _title = MutableStateFlow("") @@ -122,6 +124,8 @@ constructor( ?.map { it.first } ?.take(6) ?: listOf("👍", "👎", "😂", "🔥", "❤️", "😮") + val homoglyphEncodingEnabled = homoglyphEncodingPrefs.getHomoglyphEncodingEnabledChangesFlow() + init { val contactKey = savedStateHandle.get("contactKey") if (contactKey != null) { @@ -204,8 +208,20 @@ constructor( } } } + + // Applying homoglyph encoding to the transmitted string if user has activated the feature + // In most cases the value in "str" parameter will already contain the correct + // transformed string from the text input. This call here added to make sure that + // the feature is effective across all possible message paths (quick-chat, reply, etc.) + val dataPacketText: String = + if (homoglyphEncodingPrefs.homoglyphEncodingEnabled) { + HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(str) + } else { + str + } + val p = - DataPacket(dest, channel ?: 0, str, replyId).apply { + DataPacket(dest, channel ?: 0, dataPacketText, replyId).apply { from = ourNodeInfo.value?.user?.id ?: DataPacket.ID_LOCAL } sendDataPacket(p) diff --git a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt new file mode 100644 index 000000000..f521c5e07 --- /dev/null +++ b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.messaging + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class HomoglyphCharacterTransformTest { + + @Test + fun `optimizeUtf8StringWithHomoglyphs shrinks binary size of cyrillic text containing some homoglyphs`() { + val testString = "Мештастик - это проект с открытым исходным кодом" + val transformedTestString = HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(testString) + val testStringBytes = testString.toByteArray(charset = Charsets.UTF_8) + val transformedTestStringBytes = transformedTestString.toByteArray(charset = Charsets.UTF_8) + val transformedStringBinarySizeShrinked = transformedTestStringBytes.size < testStringBytes.size + assertTrue(transformedStringBinarySizeShrinked) + } + + @Test + fun `optimizeUtf8StringWithHomoglyphs shrinks binary size in half of cyrillic text containing only homoglyphs`() { + val testString = "Косуха" + val transformedTestString = HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(testString) + val testStringBytes = testString.toByteArray(charset = Charsets.UTF_8) + val transformedTestStringBytes = transformedTestString.toByteArray(charset = Charsets.UTF_8) + assertEquals(transformedTestStringBytes.size, testStringBytes.size / 2) + } + + @Test + fun `optimizeUtf8StringWithHomoglyphs does not transform cyrillic text without any homoglyphs`() { + val testString = "Близкий" + val transformedTestString = HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(testString) + assertEquals(transformedTestString, testString) + } + + @Test + fun `optimizeUtf8StringWithHomoglyphs does not transform latin text message`() { + val testString = "Meshtastic is an open source, off-grid, decentralized mesh network" + val transformedTestString = HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(testString) + assertEquals(transformedTestString, testString) + } + + @Test + fun `optimizeUtf8StringWithHomoglyphs does not transform characters impossible to present by latin letters`() { + val testString = "ميشتاستيك هو مصدر مفتوح ، خارج الشبكة ، شبكة شبكة" + val transformedTestString = HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(testString) + assertEquals(transformedTestString, testString) + } +} diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt index 35e93f25b..316a12743 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight +import androidx.compose.material.icons.filled.Abc import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.rounded.AppSettingsAlt import androidx.compose.material.icons.rounded.FormatPaint @@ -102,6 +103,7 @@ import org.meshtastic.core.strings.theme import org.meshtastic.core.strings.theme_dark import org.meshtastic.core.strings.theme_light import org.meshtastic.core.strings.theme_system +import org.meshtastic.core.strings.use_homoglyph_characters_encoding import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.ListItem import org.meshtastic.core.ui.component.MainAppBar @@ -315,6 +317,15 @@ fun SettingsScreen( onClick = { settingsViewModel.setProvideLocation(!provideLocation) }, ) + val homoglyphEncodingEnabled by + viewModel.homoglyphEncodingEnabledFlow.collectAsStateWithLifecycle(false) + SwitchListItem( + text = stringResource(Res.string.use_homoglyph_characters_encoding), + checked = homoglyphEncodingEnabled, + leadingIcon = Icons.Default.Abc, + onClick = { viewModel.toggleHomoglyphCharactersEncodingEnabled() }, + ) + val settingsLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {} diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index cd5ac5a51..24c9e77f0 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -57,6 +57,7 @@ import org.meshtastic.core.model.Position import org.meshtastic.core.model.util.toChannelSet import org.meshtastic.core.navigation.SettingsRoutes import org.meshtastic.core.prefs.analytics.AnalyticsPrefs +import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import org.meshtastic.core.prefs.map.MapConsentPrefs import org.meshtastic.core.service.ConnectionState import org.meshtastic.core.service.IMeshService @@ -117,6 +118,7 @@ constructor( private val locationRepository: LocationRepository, private val mapConsentPrefs: MapConsentPrefs, private val analyticsPrefs: AnalyticsPrefs, + private val homoglyphEncodingPrefs: HomoglyphPrefs, ) : ViewModel() { private val meshService: IMeshService? get() = serviceRepository.meshService @@ -127,6 +129,12 @@ constructor( analyticsPrefs.analyticsAllowed = !analyticsPrefs.analyticsAllowed } + val homoglyphEncodingEnabledFlow = homoglyphEncodingPrefs.getHomoglyphEncodingEnabledChangesFlow() + + fun toggleHomoglyphCharactersEncodingEnabled() { + homoglyphEncodingPrefs.homoglyphEncodingEnabled = !homoglyphEncodingPrefs.homoglyphEncodingEnabled + } + private val destNum = savedStateHandle.get("destNum") ?: runCatching { savedStateHandle.toRoute().destNum }.getOrNull()