mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-28 06:26:39 +10:00
respect smaz encoding in message byte length calculation.
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user