From 09ebba321d704b8547793eaa76d5370465cbb2c4 Mon Sep 17 00:00:00 2001 From: ericz Date: Sat, 11 Apr 2026 18:48:43 +0200 Subject: [PATCH] respect smaz encoding in message byte length calculation. forgot sending button check, for contacts. --- lib/connector/meshcore_connector.dart | 18 +++++++++++------- lib/helpers/utf8_length_limiter.dart | 19 ++++++++++++++++--- lib/screens/channel_chat_screen.dart | 11 ++++++++++- lib/screens/chat_screen.dart | 13 ++++++++++++- lib/widgets/byte_count_input.dart | 15 +++++++++++++-- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b432277..fceee15 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2994,13 +2994,7 @@ class MeshCoreConnector extends ChangeNotifier { _pendingChannelSentQueue.add(message.messageId); notifyListeners(); - final trimmed = text.trim(); - final isStructuredPayload = - trimmed.startsWith('g:') || trimmed.startsWith('m:'); - final outboundText = - (isChannelSmazEnabled(channel.index) && !isStructuredPayload) - ? Smaz.encodeIfSmaller(text) - : text; + final outboundText = prepareChannelOutboundText(channel.index, text); await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime); await sendFrame( buildSendChannelTextMsgFrame(channel.index, outboundText), @@ -4452,6 +4446,16 @@ class MeshCoreConnector extends ChangeNotifier { return text; } + String prepareChannelOutboundText(int channelIndex, String text) { + final trimmed = text.trim(); + final isStructuredPayload = + trimmed.startsWith('g:') || trimmed.startsWith('m:'); + if (!isStructuredPayload && isChannelSmazEnabled(channelIndex)) { + return Smaz.encodeIfSmaller(text); + } + return text; + } + String _channelDisplayName(int channelIndex) { for (final channel in _channels) { if (channel.index != channelIndex) continue; diff --git a/lib/helpers/utf8_length_limiter.dart b/lib/helpers/utf8_length_limiter.dart index c6acdd2..4188ec6 100644 --- a/lib/helpers/utf8_length_limiter.dart +++ b/lib/helpers/utf8_length_limiter.dart @@ -4,8 +4,14 @@ import 'package:flutter/services.dart'; class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter { final int maxBytes; + final String Function(String)? encoder; - const Utf8LengthLimitingTextInputFormatter(this.maxBytes); + const Utf8LengthLimitingTextInputFormatter(this.maxBytes, {this.encoder}); + + int _effectiveByteLength(String text) { + final effective = encoder != null ? encoder!(text) : text; + return utf8.encode(effective).length; + } @override TextEditingValue formatEditUpdate( @@ -13,8 +19,7 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter { TextEditingValue newValue, ) { if (maxBytes <= 0) return oldValue; - final bytes = utf8.encode(newValue.text); - if (bytes.length <= maxBytes) return newValue; + if (_effectiveByteLength(newValue.text) <= maxBytes) return newValue; final truncated = _truncateToMaxBytes(newValue.text, maxBytes); return TextEditingValue( @@ -25,6 +30,14 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter { } String _truncateToMaxBytes(String text, int limit) { + if (encoder != null) { + final runes = text.runes.toList(); + while (runes.isNotEmpty && + _effectiveByteLength(String.fromCharCodes(runes)) > maxBytes) { + runes.removeLast(); + } + return String.fromCharCodes(runes); + } final buffer = StringBuffer(); var used = 0; for (final rune in text.runes) { diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 4efffc0..7989085 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -10,6 +10,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../utils/platform_info.dart'; import '../helpers/chat_scroll_controller.dart'; +import '../helpers/smaz.dart'; import '../connector/meshcore_protocol.dart'; import '../helpers/gif_helper.dart'; import '../helpers/reaction_helper.dart'; @@ -1099,6 +1100,10 @@ class _ChannelChatScreenState extends State { focusNode: _textFieldFocusNode, hintText: context.l10n.chat_typeMessage, onSubmitted: (_) => _sendMessage(), + encoder: + connector.isChannelSmazEnabled(widget.channel.index) + ? Smaz.encodeIfSmaller + : null, decoration: InputDecoration( hintText: context.l10n.chat_typeMessage, border: OutlineInputBorder( @@ -1193,7 +1198,11 @@ class _ChannelChatScreenState extends State { } final maxBytes = maxChannelMessageBytes(connector.selfName); - if (utf8.encode(messageText).length > maxBytes) { + final outboundText = connector.prepareChannelOutboundText( + widget.channel.index, + messageText, + ); + if (utf8.encode(outboundText).length > maxBytes) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))), ); diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 613f57c..6714df4 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -14,6 +14,7 @@ import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../helpers/reaction_helper.dart'; +import '../helpers/smaz.dart'; import '../widgets/message_status_icon.dart'; import '../helpers/chat_scroll_controller.dart'; import '../helpers/gif_helper.dart'; @@ -572,6 +573,12 @@ class _ChatScreenState extends State { focusNode: _textFieldFocusNode, hintText: context.l10n.chat_typeMessage, onSubmitted: (_) => _sendMessage(connector), + encoder: + connector.isContactSmazEnabled( + widget.contact.publicKeyHex, + ) + ? Smaz.encodeIfSmaller + : null, decoration: InputDecoration( hintText: context.l10n.chat_typeMessage, border: OutlineInputBorder( @@ -673,7 +680,11 @@ class _ChatScreenState extends State { } } final maxBytes = maxContactMessageBytes(); - if (utf8.encode(outgoingText).length > maxBytes) { + final outboundText = connector.prepareContactOutboundText( + _resolveContact(connector), + outgoingText, + ); + if (utf8.encode(outboundText).length > maxBytes) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))), ); diff --git a/lib/widgets/byte_count_input.dart b/lib/widgets/byte_count_input.dart index 5832ae8..bfb5fcc 100644 --- a/lib/widgets/byte_count_input.dart +++ b/lib/widgets/byte_count_input.dart @@ -50,6 +50,10 @@ class ByteCountedTextField extends StatelessWidget { /// Whether to hide the counter when the field is empty (default `true`). final bool hideCounterWhenEmpty; + /// Optional encoder function to transform text before byte counting/limiting. + /// If provided, byte limits and counters will use the encoded text length. + final String Function(String)? encoder; + const ByteCountedTextField({ super.key, required this.maxBytes, @@ -64,6 +68,7 @@ class ByteCountedTextField extends StatelessWidget { this.warningThreshold = 0.7, this.errorThreshold = 0.9, this.hideCounterWhenEmpty = true, + this.encoder, }); @override @@ -71,7 +76,10 @@ class ByteCountedTextField extends StatelessWidget { return ValueListenableBuilder( valueListenable: controller, builder: (context, value, _) { - final usedBytes = utf8.encode(value.text).length; + final effectiveText = encoder != null + ? encoder!(value.text) + : value.text; + final usedBytes = utf8.encode(effectiveText).length; final ratio = maxBytes > 0 ? usedBytes / maxBytes : 0.0; final showCounter = !(hideCounterWhenEmpty && value.text.isEmpty); @@ -90,7 +98,10 @@ class ByteCountedTextField extends StatelessWidget { focusNode: focusNode, inputFormatters: [ ...extraFormatters, - Utf8LengthLimitingTextInputFormatter(maxBytes), + Utf8LengthLimitingTextInputFormatter( + maxBytes, + encoder: encoder, + ), ], textCapitalization: textCapitalization, decoration: