respect smaz encoding in message byte length calculation.

chore/offband-rebrand
ericz 3 months ago committed by Enot (ded) Skelly
parent e97fb9bd24
commit b572314ae9
No known key found for this signature in database
GPG Key ID: 2FE5B19B03656304

@ -2994,13 +2994,7 @@ class MeshCoreConnector extends ChangeNotifier {
_pendingChannelSentQueue.add(message.messageId); _pendingChannelSentQueue.add(message.messageId);
notifyListeners(); notifyListeners();
final trimmed = text.trim(); final outboundText = prepareChannelOutboundText(channel.index, text);
final isStructuredPayload =
trimmed.startsWith('g:') || trimmed.startsWith('m:');
final outboundText =
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
? Smaz.encodeIfSmaller(text)
: text;
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime); await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
await sendFrame( await sendFrame(
buildSendChannelTextMsgFrame(channel.index, outboundText), buildSendChannelTextMsgFrame(channel.index, outboundText),
@ -4452,6 +4446,16 @@ class MeshCoreConnector extends ChangeNotifier {
return text; 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) { String _channelDisplayName(int channelIndex) {
for (final channel in _channels) { for (final channel in _channels) {
if (channel.index != channelIndex) continue; if (channel.index != channelIndex) continue;

@ -4,8 +4,14 @@ import 'package:flutter/services.dart';
class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter { class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
final int maxBytes; 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 @override
TextEditingValue formatEditUpdate( TextEditingValue formatEditUpdate(
@ -13,8 +19,7 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
TextEditingValue newValue, TextEditingValue newValue,
) { ) {
if (maxBytes <= 0) return oldValue; if (maxBytes <= 0) return oldValue;
final bytes = utf8.encode(newValue.text); if (_effectiveByteLength(newValue.text) <= maxBytes) return newValue;
if (bytes.length <= maxBytes) return newValue;
final truncated = _truncateToMaxBytes(newValue.text, maxBytes); final truncated = _truncateToMaxBytes(newValue.text, maxBytes);
return TextEditingValue( return TextEditingValue(
@ -25,6 +30,14 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
} }
String _truncateToMaxBytes(String text, int limit) { 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(); final buffer = StringBuffer();
var used = 0; var used = 0;
for (final rune in text.runes) { for (final rune in text.runes) {

@ -10,6 +10,7 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../helpers/chat_scroll_controller.dart'; import '../helpers/chat_scroll_controller.dart';
import '../helpers/smaz.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../helpers/gif_helper.dart'; import '../helpers/gif_helper.dart';
import '../helpers/reaction_helper.dart'; import '../helpers/reaction_helper.dart';
@ -1099,6 +1100,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
focusNode: _textFieldFocusNode, focusNode: _textFieldFocusNode,
hintText: context.l10n.chat_typeMessage, hintText: context.l10n.chat_typeMessage,
onSubmitted: (_) => _sendMessage(), onSubmitted: (_) => _sendMessage(),
encoder:
connector.isChannelSmazEnabled(widget.channel.index)
? Smaz.encodeIfSmaller
: null,
decoration: InputDecoration( decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage, hintText: context.l10n.chat_typeMessage,
border: OutlineInputBorder( border: OutlineInputBorder(
@ -1194,7 +1199,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
} }
final maxBytes = maxChannelMessageBytes(connector.selfName); 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) {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.chat_messageTooLong(maxBytes)), content: Text(context.l10n.chat_messageTooLong(maxBytes)),

@ -14,6 +14,7 @@ import 'package:latlong2/latlong.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart'; import '../helpers/reaction_helper.dart';
import '../helpers/smaz.dart';
import '../widgets/message_status_icon.dart'; import '../widgets/message_status_icon.dart';
import '../helpers/chat_scroll_controller.dart'; import '../helpers/chat_scroll_controller.dart';
import '../helpers/gif_helper.dart'; import '../helpers/gif_helper.dart';
@ -573,6 +574,12 @@ class _ChatScreenState extends State<ChatScreen> {
focusNode: _textFieldFocusNode, focusNode: _textFieldFocusNode,
hintText: context.l10n.chat_typeMessage, hintText: context.l10n.chat_typeMessage,
onSubmitted: (_) => _sendMessage(connector), onSubmitted: (_) => _sendMessage(connector),
encoder:
connector.isContactSmazEnabled(
widget.contact.publicKeyHex,
)
? Smaz.encodeIfSmaller
: null,
decoration: InputDecoration( decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage, hintText: context.l10n.chat_typeMessage,
border: OutlineInputBorder( border: OutlineInputBorder(
@ -674,7 +681,11 @@ class _ChatScreenState extends State<ChatScreen> {
} }
} }
final maxBytes = maxContactMessageBytes(); final maxBytes = maxContactMessageBytes();
if (utf8.encode(outgoingText).length > maxBytes) { final outboundText = connector.prepareContactOutboundText(
_resolveContact(connector),
outgoingText,
);
if (utf8.encode(outboundText).length > maxBytes) {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.chat_messageTooLong(maxBytes)), content: Text(context.l10n.chat_messageTooLong(maxBytes)),

@ -50,6 +50,10 @@ class ByteCountedTextField extends StatelessWidget {
/// Whether to hide the counter when the field is empty (default `true`). /// Whether to hide the counter when the field is empty (default `true`).
final bool hideCounterWhenEmpty; 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({ const ByteCountedTextField({
super.key, super.key,
required this.maxBytes, required this.maxBytes,
@ -64,6 +68,7 @@ class ByteCountedTextField extends StatelessWidget {
this.warningThreshold = 0.7, this.warningThreshold = 0.7,
this.errorThreshold = 0.9, this.errorThreshold = 0.9,
this.hideCounterWhenEmpty = true, this.hideCounterWhenEmpty = true,
this.encoder,
}); });
@override @override
@ -71,7 +76,10 @@ class ByteCountedTextField extends StatelessWidget {
return ValueListenableBuilder<TextEditingValue>( return ValueListenableBuilder<TextEditingValue>(
valueListenable: controller, valueListenable: controller,
builder: (context, value, _) { 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 ratio = maxBytes > 0 ? usedBytes / maxBytes : 0.0;
final showCounter = !(hideCounterWhenEmpty && value.text.isEmpty); final showCounter = !(hideCounterWhenEmpty && value.text.isEmpty);
@ -90,7 +98,10 @@ class ByteCountedTextField extends StatelessWidget {
focusNode: focusNode, focusNode: focusNode,
inputFormatters: [ inputFormatters: [
...extraFormatters, ...extraFormatters,
Utf8LengthLimitingTextInputFormatter(maxBytes), Utf8LengthLimitingTextInputFormatter(
maxBytes,
encoder: encoder,
),
], ],
textCapitalization: textCapitalization, textCapitalization: textCapitalization,
decoration: decoration:

Loading…
Cancel
Save

Powered by TurnKey Linux.