diff --git a/lib/helpers/utf8_length_limiter.dart b/lib/helpers/utf8_length_limiter.dart index c6acdd2..f706d49 100644 --- a/lib/helpers/utf8_length_limiter.dart +++ b/lib/helpers/utf8_length_limiter.dart @@ -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); } } diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 809d219..3ee4afe 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -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 => 'Присъедини се към стаята'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9967963..15c7cd3 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -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'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 47a80ce..c2c99d1 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -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'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index b2d25b2..14b2dae 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 09793b8..7e03a2b 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -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'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 84014de..c5c69ed 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 3083abb..05d09e4 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 0399c4b..50f409e 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 4060b55..7d0b367 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -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 => 'Присоединиться к комнате'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 715bb63..ece2f27 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -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ť'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 528b85f..7a676e9 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index d655371..ed968d5 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -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'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index f9b2d9b..244ab38 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -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 => 'Приєднатися до кімнати'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 6fc7634..b18f7b3 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -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 => '加入房间'; diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 45aee76..16308b0 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -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 { ); } + 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 { 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( @@ -916,23 +923,39 @@ class _ChannelChatScreenState extends State { ); 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 _confirmPublicShare(String channelLabel) async { + final result = await showDialog( + 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 { Widget _buildMessageComposer() { final connector = context.watch(); 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 { controller: _textController, focusNode: _textFieldFocusNode, inputFormatters: [ - Utf8LengthLimitingTextInputFormatter(maxBytes), + Utf8LengthLimitingTextInputFormatter( + maxBytes, + encoder: smazEncoder, + ), ], textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( @@ -1305,6 +1334,7 @@ class _ChannelChatScreenState extends State { context: context, isScrollControlled: true, builder: (context) => EmojiPicker( + title: context.l10n.chat_addReaction, onEmojiSelected: (emoji) { _sendReaction(message, emoji); }, diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 7e25804..8f002b3 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -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 { 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 { controller: _textController, focusNode: _textFieldFocusNode, inputFormatters: [ - Utf8LengthLimitingTextInputFormatter(maxBytes), + Utf8LengthLimitingTextInputFormatter( + maxBytes, + encoder: smazEncoder, + ), ], textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( @@ -497,6 +505,7 @@ class _ChatScreenState extends State { 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 { 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( @@ -567,22 +580,6 @@ class _ChatScreenState extends State { 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 { context: context, isScrollControlled: true, builder: (context) => EmojiPicker( + title: context.l10n.chat_addReaction, onEmojiSelected: (emoji) { _sendReaction(message, senderContact, emoji); }, diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 7d0aa22..1723754 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1486,6 +1486,10 @@ class _MapScreenState extends State { String defaultLabel, ) async { final controller = TextEditingController(text: defaultLabel); + controller.selection = TextSelection( + baseOffset: 0, + extentOffset: controller.text.length, + ); return showDialog( context: context, builder: (dialogContext) => AlertDialog( diff --git a/lib/widgets/emoji_picker.dart b/lib/widgets/emoji_picker.dart index 87fd1c9..a61a0dd 100644 --- a/lib/widgets/emoji_picker.dart +++ b/lib/widgets/emoji_picker.dart @@ -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 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, diff --git a/untranslated.json b/untranslated.json index f86f869..9e26dfe 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,29 +1 @@ -{ - "bg": [], - - "de": [], - - "es": [], - - "fr": [], - - "it": [], - - "nl": [], - - "pl": [], - - "pt": [], - - "ru": [], - - "sk": [], - - "sl": [], - - "sv": [], - - "uk": [], - - "zh": [] -} +{} \ No newline at end of file