From db39522bbb446a6e89b0715564448514862a457f Mon Sep 17 00:00:00 2001 From: infi Date: Sat, 6 Jun 2026 18:40:39 +0200 Subject: [PATCH] feat: handle PUA characters --- app/src/main/java/chat/stoat/composables/chat/Message.kt | 8 +++++--- app/src/main/java/chat/stoat/internals/text/Gigamoji.kt | 1 + app/src/main/java/chat/stoat/internals/text/PUAChars.kt | 6 ++++++ .../screens/chat/views/channel/ChannelScreenViewModel.kt | 3 ++- 4 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/chat/stoat/internals/text/PUAChars.kt diff --git a/app/src/main/java/chat/stoat/composables/chat/Message.kt b/app/src/main/java/chat/stoat/composables/chat/Message.kt index c89bb144..f543939b 100644 --- a/app/src/main/java/chat/stoat/composables/chat/Message.kt +++ b/app/src/main/java/chat/stoat/composables/chat/Message.kt @@ -87,6 +87,7 @@ import chat.stoat.core.model.schemas.User import chat.stoat.internals.text.Gigamoji import chat.stoat.internals.text.GigamojiState import chat.stoat.internals.text.MessageProcessor +import chat.stoat.internals.text.stripPUAChars import chat.stoat.persistence.KVStorage import com.mikepenz.markdown.model.State import kotlinx.coroutines.launch @@ -438,9 +439,10 @@ fun Message( key(message.content) { message.content?.let { - if (message.content!!.isBlank()) return@let // if only an attachment is sent + val content = it.stripPUAChars() + if (content.isBlank()) return@let // if only an attachment is sent - val gigamoji = Gigamoji.useGigamojiForMessage(message.content!!) + val gigamoji = Gigamoji.useGigamojiForMessage(content) val fontSizeMultiplier = when (gigamoji) { GigamojiState.Single -> 5f GigamojiState.Multiple -> 2f @@ -455,7 +457,7 @@ fun Message( ) } else { ChatMarkdown( - content = message.content!!, + content = content, serverId = StoatAPI.channelCache[message.channel]?.server, fontSizeMultiplier = fontSizeMultiplier, ) diff --git a/app/src/main/java/chat/stoat/internals/text/Gigamoji.kt b/app/src/main/java/chat/stoat/internals/text/Gigamoji.kt index a025f770..8b429a71 100644 --- a/app/src/main/java/chat/stoat/internals/text/Gigamoji.kt +++ b/app/src/main/java/chat/stoat/internals/text/Gigamoji.kt @@ -26,6 +26,7 @@ object Gigamoji { Character.isWhitespace(codepoint) -> lastWasZwj = false codepoint == 0x200D -> lastWasZwj = true codepoint == 0xFE0F || codepoint == 0xFE0E || codepoint == 0x20E3 -> Unit + codepoint in PUA_MIN..PUA_MAX -> Unit codepoint in 0x1F3FB..0x1F3FF -> lastWasZwj = false MessageProcessor.emoji.codepointIsEmoji(codepoint) -> { if (!lastWasZwj) count++ diff --git a/app/src/main/java/chat/stoat/internals/text/PUAChars.kt b/app/src/main/java/chat/stoat/internals/text/PUAChars.kt new file mode 100644 index 00000000..d97326c5 --- /dev/null +++ b/app/src/main/java/chat/stoat/internals/text/PUAChars.kt @@ -0,0 +1,6 @@ +package chat.stoat.internals.text + +internal const val PUA_MIN = 0xE0E0 +internal const val PUA_MAX = 0xE0E6 + +internal fun String.stripPUAChars(): String = filter { it.code !in PUA_MIN..PUA_MAX } diff --git a/app/src/main/java/chat/stoat/screens/chat/views/channel/ChannelScreenViewModel.kt b/app/src/main/java/chat/stoat/screens/chat/views/channel/ChannelScreenViewModel.kt index 200e35e4..38c4f82c 100644 --- a/app/src/main/java/chat/stoat/screens/chat/views/channel/ChannelScreenViewModel.kt +++ b/app/src/main/java/chat/stoat/screens/chat/views/channel/ChannelScreenViewModel.kt @@ -49,6 +49,7 @@ import chat.stoat.callbacks.UiCallbacks import chat.stoat.core.model.schemas.Channel import chat.stoat.core.model.schemas.Message import chat.stoat.internals.text.MessageProcessor +import chat.stoat.internals.text.stripPUAChars import chat.stoat.composables.markdown.prose.easyLineBreaks import chat.stoat.markdown.StoatMarkdownFlavour import chat.stoat.persistence.KVStorage @@ -264,7 +265,7 @@ class ChannelScreenViewModel( private suspend fun parseAst(content: String?): State? = content?.takeIf { it.isNotBlank() }?.let { c -> - val prepared = injectMassMentionMarkers(easyLineBreaks(c)) + val prepared = injectMassMentionMarkers(easyLineBreaks(c.stripPUAChars())) parseMarkdownFlow(prepared, flavour = StoatMarkdownFlavour(prepared)) .first { it !is State.Loading } }