mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
resolved codex suggestion. Added smaz compresson aware length limitter. on Location share ask for confirmation on public channel and preselect label input field for easier editing.
This commit is contained in:
parent
35cc73a2f7
commit
cd24bb82a1
20 changed files with 135 additions and 112 deletions
|
|
@ -2,10 +2,32 @@ import 'dart:convert';
|
|||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
String truncateToUtf8Bytes(String text, int maxBytes) {
|
||||
if (maxBytes <= 0) return '';
|
||||
|
||||
final buffer = StringBuffer();
|
||||
var usedBytes = 0;
|
||||
for (final rune in text.runes) {
|
||||
final character = String.fromCharCode(rune);
|
||||
final characterBytes = utf8.encode(character).length;
|
||||
if (usedBytes + characterBytes > maxBytes) break;
|
||||
buffer.write(character);
|
||||
usedBytes += characterBytes;
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
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,10 +35,9 @@ 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);
|
||||
final truncated = _truncate(newValue.text);
|
||||
return TextEditingValue(
|
||||
text: truncated,
|
||||
selection: TextSelection.collapsed(offset: truncated.length),
|
||||
|
|
@ -24,16 +45,13 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
|||
);
|
||||
}
|
||||
|
||||
String _truncateToMaxBytes(String text, int limit) {
|
||||
final buffer = StringBuffer();
|
||||
var used = 0;
|
||||
for (final rune in text.runes) {
|
||||
final char = String.fromCharCode(rune);
|
||||
final charBytes = utf8.encode(char).length;
|
||||
if (used + charBytes > limit) break;
|
||||
buffer.write(char);
|
||||
used += charBytes;
|
||||
String _truncate(String text) {
|
||||
if (encoder == null) return truncateToUtf8Bytes(text, maxBytes);
|
||||
final runes = text.runes.toList();
|
||||
while (runes.isNotEmpty &&
|
||||
_effectiveByteLength(String.fromCharCodes(runes)) > maxBytes) {
|
||||
runes.removeLast();
|
||||
}
|
||||
return buffer.toString();
|
||||
return String.fromCharCodes(runes);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1550,7 +1550,7 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
String get map_sharedPin => 'Споделено копие';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Споделено';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Присъедини се към стаята';
|
||||
|
|
|
|||
|
|
@ -1550,7 +1550,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get map_sharedPin => 'Gemeinsames Passwort';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Geteilt';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Beitreten Sie dem Raum';
|
||||
|
|
|
|||
|
|
@ -1548,7 +1548,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
String get map_sharedPin => 'Pin compartido';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Compartido';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Únete a la sala';
|
||||
|
|
|
|||
|
|
@ -1555,7 +1555,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get map_sharedPin => 'Clé partagée';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Partagé';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Rejoindre la salle';
|
||||
|
|
|
|||
|
|
@ -1547,7 +1547,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
String get map_sharedPin => 'Condividi PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Condiviso';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Unisciti alla stanza';
|
||||
|
|
|
|||
|
|
@ -1540,7 +1540,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get map_sharedPin => 'Gedeelde pin';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Gedeeld';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Sluit Kamer';
|
||||
|
|
|
|||
|
|
@ -1549,7 +1549,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
String get map_sharedPin => 'Podzielony PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Udostępnione';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Dołącz do pokoju';
|
||||
|
|
|
|||
|
|
@ -1549,7 +1549,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
String get map_sharedPin => 'Pin compartilhado';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Compartilhado';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Junte-se à Sala';
|
||||
|
|
|
|||
|
|
@ -1551,7 +1551,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
String get map_sharedPin => 'Общая метка';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Поделено';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Присоединиться к комнате';
|
||||
|
|
|
|||
|
|
@ -1543,7 +1543,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
String get map_sharedPin => 'Zdieľaný PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Zdieľané';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Pripojiť miestnosť';
|
||||
|
|
|
|||
|
|
@ -1536,7 +1536,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
String get map_sharedPin => 'Deljeno naslovno geslo';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Deljeno';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Pridružiti sobo';
|
||||
|
|
|
|||
|
|
@ -1533,7 +1533,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
String get map_sharedPin => 'Delad PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Delad';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Gå med i rum';
|
||||
|
|
|
|||
|
|
@ -1548,7 +1548,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||
String get map_sharedPin => 'Спільний пін';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => 'Поділено';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Приєднатися до кімнати';
|
||||
|
|
|
|||
|
|
@ -1054,13 +1054,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
String get chat_sendGif => '发送 GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Insert emoji';
|
||||
String get chat_insertEmoji => '插入表情';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Share location';
|
||||
String get chat_shareLocation => '分享位置';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Location not available';
|
||||
String get chat_locationUnavailable => '位置不可用';
|
||||
|
||||
@override
|
||||
String get chat_reply => '回复';
|
||||
|
|
@ -1459,7 +1459,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
String get map_sharedPin => '共享标记';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
String get map_sharedAt => '已分享';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => '加入房间';
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import '../connector/meshcore_protocol.dart';
|
|||
import '../helpers/link_handler.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/utf8_length_limiter.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/channel_message.dart';
|
||||
|
|
@ -827,28 +828,30 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
void _insertTextAtCursor(String text) {
|
||||
final currentValue = _textController.value;
|
||||
final selection = currentValue.selection;
|
||||
final newText = selection.isValid
|
||||
? currentValue.text.replaceRange(selection.start, selection.end, text)
|
||||
: currentValue.text + text;
|
||||
final caret =
|
||||
(selection.isValid ? selection.start : currentValue.text.length) +
|
||||
text.length;
|
||||
|
||||
_textController.value = currentValue.copyWith(
|
||||
text: newText,
|
||||
selection: TextSelection.collapsed(offset: caret),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEmojiPickerForComposer(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => EmojiPicker(
|
||||
title: context.l10n.chat_insertEmoji,
|
||||
onEmojiSelected: (emoji) {
|
||||
final currentValue = _textController.value;
|
||||
final selection = currentValue.selection;
|
||||
final newText = selection.isValid
|
||||
? currentValue.text.replaceRange(
|
||||
selection.start,
|
||||
selection.end,
|
||||
emoji,
|
||||
)
|
||||
: currentValue.text + emoji;
|
||||
final caret =
|
||||
(selection.isValid ? selection.start : currentValue.text.length) +
|
||||
emoji.length;
|
||||
_textController.value = currentValue.copyWith(
|
||||
text: newText,
|
||||
selection: TextSelection.collapsed(offset: caret),
|
||||
);
|
||||
_insertTextAtCursor(emoji);
|
||||
_textFieldFocusNode.requestFocus();
|
||||
},
|
||||
),
|
||||
|
|
@ -875,7 +878,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
final defaultLabel =
|
||||
'${connector.deviceDisplayName} ${DateTime.now().toUtc().toIso8601String()}';
|
||||
final controller = TextEditingController(
|
||||
text: _truncateToUtf8Bytes(defaultLabel, maxLabelBytes),
|
||||
text: truncateToUtf8Bytes(defaultLabel, maxLabelBytes),
|
||||
);
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: controller.text.length,
|
||||
);
|
||||
|
||||
var label = await showDialog<String>(
|
||||
|
|
@ -916,23 +923,39 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.channel.isPublicChannel) {
|
||||
final channelLabel = widget.channel.name.isEmpty
|
||||
? context.l10n.channels_channelIndex(widget.channel.index)
|
||||
: widget.channel.name;
|
||||
final canShare = await _confirmPublicShare(channelLabel);
|
||||
if (!mounted || !canShare) return;
|
||||
}
|
||||
|
||||
connector.sendChannelMessage(widget.channel, markerText);
|
||||
}
|
||||
|
||||
String _truncateToUtf8Bytes(String text, int maxBytes) {
|
||||
if (maxBytes <= 0) return '';
|
||||
|
||||
final buffer = StringBuffer();
|
||||
var usedBytes = 0;
|
||||
for (final rune in text.runes) {
|
||||
final character = String.fromCharCode(rune);
|
||||
final characterBytes = utf8.encode(character).length;
|
||||
if (usedBytes + characterBytes > maxBytes) break;
|
||||
buffer.write(character);
|
||||
usedBytes += characterBytes;
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
Future<bool> _confirmPublicShare(String channelLabel) async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.l10n.map_publicLocationShare),
|
||||
content: Text(
|
||||
context.l10n.map_publicLocationShareConfirm(channelLabel),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: Text(context.l10n.common_share),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
Widget _buildAvatar(String senderName) {
|
||||
|
|
@ -1043,6 +1066,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
Widget _buildMessageComposer() {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final maxBytes = maxChannelMessageBytes(connector.selfName);
|
||||
final smazEncoder = connector.isChannelSmazEnabled(widget.channel.index)
|
||||
? Smaz.encodeIfSmaller
|
||||
: null;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
|
@ -1169,7 +1195,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
controller: _textController,
|
||||
focusNode: _textFieldFocusNode,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(maxBytes),
|
||||
Utf8LengthLimitingTextInputFormatter(
|
||||
maxBytes,
|
||||
encoder: smazEncoder,
|
||||
),
|
||||
],
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
|
|
@ -1305,6 +1334,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => EmojiPicker(
|
||||
title: context.l10n.chat_addReaction,
|
||||
onEmojiSelected: (emoji) {
|
||||
_sendReaction(message, emoji);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import '../widgets/message_status_icon.dart';
|
|||
import '../helpers/chat_scroll_controller.dart';
|
||||
import '../helpers/link_handler.dart';
|
||||
import '../helpers/utf8_length_limiter.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/message.dart';
|
||||
|
|
@ -336,6 +337,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
Widget _buildInputBar(MeshCoreConnector connector) {
|
||||
final maxBytes = maxContactMessageBytes();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final smazEncoder =
|
||||
connector.isContactSmazEnabled(widget.contact.publicKeyHex)
|
||||
? Smaz.encodeIfSmaller
|
||||
: null;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -442,7 +447,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
controller: _textController,
|
||||
focusNode: _textFieldFocusNode,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(maxBytes),
|
||||
Utf8LengthLimitingTextInputFormatter(
|
||||
maxBytes,
|
||||
encoder: smazEncoder,
|
||||
),
|
||||
],
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
|
|
@ -497,6 +505,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => EmojiPicker(
|
||||
title: context.l10n.chat_insertEmoji,
|
||||
onEmojiSelected: (emoji) {
|
||||
_insertTextAtCursor(emoji);
|
||||
_textFieldFocusNode.requestFocus();
|
||||
|
|
@ -524,7 +533,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
final defaultLabel =
|
||||
'${connector.deviceDisplayName} ${DateTime.now().toUtc().toIso8601String()}';
|
||||
final controller = TextEditingController(
|
||||
text: _truncateToUtf8Bytes(defaultLabel, maxLabelBytes),
|
||||
text: truncateToUtf8Bytes(defaultLabel, maxLabelBytes),
|
||||
);
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: controller.text.length,
|
||||
);
|
||||
|
||||
var label = await showDialog<String>(
|
||||
|
|
@ -567,22 +580,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
connector.sendMessage(widget.contact, markerText);
|
||||
}
|
||||
|
||||
String _truncateToUtf8Bytes(String text, int maxBytes) {
|
||||
if (maxBytes <= 0) return '';
|
||||
|
||||
final buffer = StringBuffer();
|
||||
var usedBytes = 0;
|
||||
for (final rune in text.runes) {
|
||||
final character = String.fromCharCode(rune);
|
||||
final characterBytes = utf8.encode(character).length;
|
||||
if (usedBytes + characterBytes > maxBytes) break;
|
||||
buffer.write(character);
|
||||
usedBytes += characterBytes;
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
void _showGifPicker(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
|
|
@ -1309,6 +1306,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => EmojiPicker(
|
||||
title: context.l10n.chat_addReaction,
|
||||
onEmojiSelected: (emoji) {
|
||||
_sendReaction(message, senderContact, emoji);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1486,6 +1486,10 @@ class _MapScreenState extends State<MapScreen> {
|
|||
String defaultLabel,
|
||||
) async {
|
||||
final controller = TextEditingController(text: defaultLabel);
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: controller.text.length,
|
||||
);
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import '../l10n/l10n.dart';
|
|||
|
||||
class EmojiPicker extends StatelessWidget {
|
||||
final Function(String) onEmojiSelected;
|
||||
final String? title;
|
||||
|
||||
const EmojiPicker({super.key, required this.onEmojiSelected});
|
||||
const EmojiPicker({super.key, required this.onEmojiSelected, this.title});
|
||||
|
||||
static const List<String> quickEmojis = ['👍', '❤️', '😂', '🎉', '👏', '🔥'];
|
||||
|
||||
|
|
@ -223,7 +224,7 @@ class EmojiPicker extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
l10n.chat_addReaction,
|
||||
title ?? l10n.chat_addReaction,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
|
|||
|
|
@ -1,29 +1 @@
|
|||
{
|
||||
"bg": [],
|
||||
|
||||
"de": [],
|
||||
|
||||
"es": [],
|
||||
|
||||
"fr": [],
|
||||
|
||||
"it": [],
|
||||
|
||||
"nl": [],
|
||||
|
||||
"pl": [],
|
||||
|
||||
"pt": [],
|
||||
|
||||
"ru": [],
|
||||
|
||||
"sk": [],
|
||||
|
||||
"sl": [],
|
||||
|
||||
"sv": [],
|
||||
|
||||
"uk": [],
|
||||
|
||||
"zh": []
|
||||
}
|
||||
{}
|
||||
Loading…
Add table
Add a link
Reference in a new issue