mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
Merge 74a0a3960e into 89a14c2719
This commit is contained in:
commit
3dce1151ff
46 changed files with 1384 additions and 156 deletions
|
|
@ -151,6 +151,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
Timer? _batteryPollTimer;
|
||||
Timer? _radioStatsPollTimer;
|
||||
int _radioStatsPollRefCount = 0;
|
||||
Timer? _locationSharingTimer;
|
||||
DateTime? _locationSharingEnd;
|
||||
String? _locationSharingContactKey;
|
||||
int? _locationSharingChannelIndex;
|
||||
String? _lastSharedLocationPositionKey;
|
||||
final ValueNotifier<CompanionRadioStats?> radioStatsNotifier =
|
||||
ValueNotifier<CompanionRadioStats?>(null);
|
||||
int _reconnectAttempts = 0;
|
||||
|
|
@ -375,6 +380,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
bool? get clientRepeat => _clientRepeat;
|
||||
int? get firmwareVerCode => _firmwareVerCode;
|
||||
Map<String, String>? get currentCustomVars => _currentCustomVars;
|
||||
String? get locationSharingContactKey => _locationSharingContactKey;
|
||||
int? get locationSharingChannelIndex => _locationSharingChannelIndex;
|
||||
int? get batteryMillivolts => _batteryMillivolts;
|
||||
int? get storageUsedKb => _storageUsedKb;
|
||||
int? get storageTotalKb => _storageTotalKb;
|
||||
|
|
@ -2222,6 +2229,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
_setState(MeshCoreConnectionState.disconnecting);
|
||||
_stopBatteryPolling();
|
||||
_stopRadioStatsPolling();
|
||||
_locationSharingTimer?.cancel();
|
||||
_locationSharingTimer = null;
|
||||
_locationSharingEnd = null;
|
||||
_locationSharingContactKey = null;
|
||||
_locationSharingChannelIndex = null;
|
||||
_lastSharedLocationPositionKey = null;
|
||||
|
||||
await _usbFrameSubscription?.cancel();
|
||||
_usbFrameSubscription = null;
|
||||
|
|
@ -2384,6 +2397,101 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
_radioStatsPollTimer = null;
|
||||
}
|
||||
|
||||
void startLocationSharing({
|
||||
String? contactKey,
|
||||
int? channelIndex,
|
||||
required Duration duration,
|
||||
}) {
|
||||
_locationSharingTimer?.cancel();
|
||||
_locationSharingEnd = DateTime.now().add(duration);
|
||||
_locationSharingContactKey = contactKey;
|
||||
_locationSharingChannelIndex = channelIndex;
|
||||
_lastSharedLocationPositionKey = null;
|
||||
final rawGpsInterval =
|
||||
int.tryParse(_currentCustomVars?['gps_interval'] ?? '') ?? 900;
|
||||
final gpsInterval = rawGpsInterval > 0 ? rawGpsInterval : 900;
|
||||
_sendLocationOnce(contactKey, channelIndex);
|
||||
_locationSharingTimer = Timer.periodic(Duration(seconds: gpsInterval), (_) {
|
||||
if (!isConnected || DateTime.now().isAfter(_locationSharingEnd!)) {
|
||||
stopLocationSharing();
|
||||
return;
|
||||
}
|
||||
_sendLocationOnce(contactKey, channelIndex);
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void stopLocationSharing() {
|
||||
if (_locationSharingTimer == null &&
|
||||
_locationSharingEnd == null &&
|
||||
_locationSharingContactKey == null &&
|
||||
_locationSharingChannelIndex == null &&
|
||||
_lastSharedLocationPositionKey == null) {
|
||||
return;
|
||||
}
|
||||
_locationSharingTimer?.cancel();
|
||||
_locationSharingTimer = null;
|
||||
_locationSharingEnd = null;
|
||||
_locationSharingContactKey = null;
|
||||
_locationSharingChannelIndex = null;
|
||||
_lastSharedLocationPositionKey = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static String sharedLocationPositionKey(double lat, double lon) {
|
||||
return '${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}';
|
||||
}
|
||||
|
||||
void _sendLocationOnce(String? contactKey, int? channelIndex) {
|
||||
final lat = _selfLatitude;
|
||||
final lon = _selfLongitude;
|
||||
if (lat == null || lon == null) return;
|
||||
final positionKey = sharedLocationPositionKey(lat, lon);
|
||||
final label = deviceDisplayName.replaceAll('|', '/');
|
||||
final text = 'm:$positionKey|$label|loc';
|
||||
if (contactKey != null) {
|
||||
final idx = _contacts.indexWhere((c) => c.publicKeyHex == contactKey);
|
||||
if (idx < 0) return;
|
||||
final contact = _contacts[idx];
|
||||
final resolved = resolvePathSelection(contact);
|
||||
// For timed direct sharing in flood mode, only send when location changes.
|
||||
if (resolved.useFlood && _lastSharedLocationPositionKey == positionKey) {
|
||||
return;
|
||||
}
|
||||
if (_retryService != null) {
|
||||
// Timed direct shares should participate in ACK/timeout handling.
|
||||
unawaited(
|
||||
_retryService!.sendMessageWithRetry(
|
||||
contact: contact,
|
||||
text: text,
|
||||
forceClearPathOnMaxRetry: true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
unawaited(_sendMessageDirect(contact, text, 1, now));
|
||||
}
|
||||
if (resolved.useFlood) {
|
||||
_lastSharedLocationPositionKey = positionKey;
|
||||
}
|
||||
} else if (channelIndex != null) {
|
||||
// Keep channel traffic low by suppressing repeated unchanged location packets.
|
||||
if (_lastSharedLocationPositionKey == positionKey) {
|
||||
return;
|
||||
}
|
||||
_lastSharedLocationPositionKey = positionKey;
|
||||
unawaited(
|
||||
sendFrame(
|
||||
buildSendChannelTextMsgFrame(
|
||||
channelIndex,
|
||||
prepareChannelOutboundText(channelIndex, text),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void acquireRadioStatsPolling() {
|
||||
_radioStatsPollRefCount++;
|
||||
if (_radioStatsPollRefCount == 1 && isConnected) {
|
||||
|
|
@ -2942,13 +3050,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),
|
||||
|
|
@ -4389,6 +4491,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;
|
||||
|
|
@ -5434,6 +5546,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
void _handleDisconnection() {
|
||||
_stopBatteryPolling();
|
||||
_stopRadioStatsPolling();
|
||||
_locationSharingTimer?.cancel();
|
||||
_locationSharingTimer = null;
|
||||
_locationSharingEnd = null;
|
||||
_locationSharingContactKey = null;
|
||||
_locationSharingChannelIndex = null;
|
||||
_lastSharedLocationPositionKey = null;
|
||||
_latestRadioStats = null;
|
||||
radioStatsNotifier.value = null;
|
||||
_prevTotalAirSecs = 0;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class GifHelper {
|
|||
).firstMatch(trimmed);
|
||||
return pageMatch?.group(1);
|
||||
}
|
||||
|
||||
|
||||
/// Encode a GIF in a format that parseGif() can parse.
|
||||
static String encodeGif(String gifId) {
|
||||
return 'g:$gifId';
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class ReactionHelper {
|
|||
|
||||
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
|
||||
}
|
||||
|
||||
|
||||
/// Encode a reaction message that parseReaction() can parse.
|
||||
static String encodeReaction(String hash, String emojiIndex) {
|
||||
return 'r:$hash:$emojiIndex';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -454,6 +454,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "Изпрати GIF",
|
||||
"chat_insertEmoji": "Вмъкнете емоджи",
|
||||
"chat_shareLocation": "Споделете местоположение",
|
||||
"chat_stopSharingLocationConfirm": "Да спра ли споделянето на местоположение?",
|
||||
"chat_once": "Веднъж",
|
||||
"chat_locationUnavailable": "Местоположението не е налично",
|
||||
"chat_reply": "Отговори",
|
||||
"chat_addReaction": "Добави Реакция",
|
||||
"chat_me": "Аз",
|
||||
|
|
@ -682,6 +687,7 @@
|
|||
"map_showSharedMarkers": "Покажи споделени маркери",
|
||||
"map_lastSeenTime": "Последна видяна дата",
|
||||
"map_sharedPin": "Споделено копие",
|
||||
"map_sharedAt": "Споделено",
|
||||
"map_joinRoom": "Присъедини се към стаята",
|
||||
"map_manageRepeater": "Управление на Повтарящ се Елемент",
|
||||
"mapCache_title": "Кеш на офлайн карти",
|
||||
|
|
|
|||
|
|
@ -454,6 +454,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "GIF senden",
|
||||
"chat_insertEmoji": "Emoji einfügen",
|
||||
"chat_shareLocation": "Standort teilen",
|
||||
"chat_stopSharingLocationConfirm": "Standortfreigabe beenden?",
|
||||
"chat_once": "Einmal",
|
||||
"chat_locationUnavailable": "Standort nicht verfügbar",
|
||||
"chat_reply": "Beantworten",
|
||||
"chat_addReaction": "Reaktion hinzufügen",
|
||||
"chat_me": "Ich",
|
||||
|
|
@ -682,6 +687,7 @@
|
|||
"map_showSharedMarkers": "Zeige gemeinsam genutzte Marker",
|
||||
"map_lastSeenTime": "Letzte Sichtung",
|
||||
"map_sharedPin": "Gemeinsames Passwort",
|
||||
"map_sharedAt": "Geteilt",
|
||||
"map_joinRoom": "Beitreten Sie dem Raum",
|
||||
"map_manageRepeater": "Repeater verwalten",
|
||||
"mapCache_title": "Offline-Karten-Cache",
|
||||
|
|
|
|||
|
|
@ -654,6 +654,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "Send GIF",
|
||||
"chat_insertEmoji": "Insert emoji",
|
||||
"chat_shareLocation": "Share location",
|
||||
"chat_stopSharingLocationConfirm": "Stop sharing location?",
|
||||
"chat_once": "Once",
|
||||
"chat_locationUnavailable": "Location not available",
|
||||
"chat_reply": "Reply",
|
||||
"chat_addReaction": "Add Reaction",
|
||||
"chat_me": "Me",
|
||||
|
|
@ -890,6 +895,7 @@
|
|||
"map_guessedLocation": "Guessed location",
|
||||
"map_lastSeenTime": "Last Seen Time",
|
||||
"map_sharedPin": "Shared pin",
|
||||
"map_sharedAt": "Shared",
|
||||
"map_joinRoom": "Join Room",
|
||||
"map_manageRepeater": "Manage Repeater",
|
||||
"map_tapToAdd": "Tap on nodes to add them to the path.",
|
||||
|
|
|
|||
|
|
@ -454,6 +454,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "Enviar GIF",
|
||||
"chat_insertEmoji": "Insertar emoji",
|
||||
"chat_shareLocation": "Compartir ubicación",
|
||||
"chat_stopSharingLocationConfirm": "¿Dejar de compartir la ubicación?",
|
||||
"chat_once": "Una vez",
|
||||
"chat_locationUnavailable": "Ubicación no disponible",
|
||||
"chat_reply": "Responder",
|
||||
"chat_addReaction": "Añadir Reacción",
|
||||
"chat_me": "Yo",
|
||||
|
|
@ -682,6 +687,7 @@
|
|||
"map_showSharedMarkers": "Mostrar marcadores compartidos",
|
||||
"map_lastSeenTime": "Última vez que se vio",
|
||||
"map_sharedPin": "Pin compartido",
|
||||
"map_sharedAt": "Compartido",
|
||||
"map_joinRoom": "Únete a la sala",
|
||||
"map_manageRepeater": "Gestionar Repetidor",
|
||||
"mapCache_title": "Caché de Mapa Offline",
|
||||
|
|
|
|||
|
|
@ -454,6 +454,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "Envoyer GIF",
|
||||
"chat_insertEmoji": "Insérer un emoji",
|
||||
"chat_shareLocation": "Partager la localisation",
|
||||
"chat_stopSharingLocationConfirm": "Arrêter le partage de position ?",
|
||||
"chat_once": "Une fois",
|
||||
"chat_locationUnavailable": "Localisation non disponible",
|
||||
"chat_reply": "Répondre",
|
||||
"chat_addReaction": "Ajouter une Réaction",
|
||||
"chat_me": "Moi",
|
||||
|
|
@ -682,6 +687,7 @@
|
|||
"map_showSharedMarkers": "Afficher les marqueurs partagés",
|
||||
"map_lastSeenTime": "Dernière fois vu",
|
||||
"map_sharedPin": "Clé partagée",
|
||||
"map_sharedAt": "Partagé",
|
||||
"map_joinRoom": "Rejoindre le room server",
|
||||
"map_manageRepeater": "Gérer le répéteur",
|
||||
"mapCache_title": "Cache de Carte Hors Ligne",
|
||||
|
|
|
|||
|
|
@ -628,6 +628,11 @@
|
|||
"chat_sendGif": "Küldj GIF-ot",
|
||||
"chat_reply": "Válasz",
|
||||
"chat_addReaction": "Hozzon létre reakciót",
|
||||
"chat_insertEmoji": "Emoji beszúrása",
|
||||
"chat_shareLocation": "Helyszín megosztása",
|
||||
"chat_stopSharingLocationConfirm": "Leállítja a helymegosztást?",
|
||||
"chat_once": "Egyszer",
|
||||
"chat_locationUnavailable": "A helyszín nem elérhető",
|
||||
"chat_me": "Én",
|
||||
"emojiCategorySmileys": "Emoji",
|
||||
"emojiCategoryGestures": "Testmozgások",
|
||||
|
|
@ -861,6 +866,7 @@
|
|||
"map_guessedLocation": "Tippolt hely",
|
||||
"map_lastSeenTime": "Utoljára megjelent idő",
|
||||
"map_sharedPin": "Gemeinsames PIN-kód",
|
||||
"map_sharedAt": "Megosztva",
|
||||
"map_joinRoom": "Csatlakozás a szobához",
|
||||
"map_manageRepeater": "Ellenőriző eszköz kezelése",
|
||||
"map_tapToAdd": "Nyomj meg a csomópontokhoz, hogy hozzáadd őket az útvonalhoz.",
|
||||
|
|
|
|||
|
|
@ -454,6 +454,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "Invia GIF",
|
||||
"chat_insertEmoji": "Inserisci emoji",
|
||||
"chat_shareLocation": "Condividi posizione",
|
||||
"chat_stopSharingLocationConfirm": "Interrompere la condivisione della posizione?",
|
||||
"chat_once": "Una volta",
|
||||
"chat_locationUnavailable": "Posizione non disponibile",
|
||||
"chat_reply": "Rispondi",
|
||||
"chat_addReaction": "Aggiungi Reazione",
|
||||
"chat_me": "Me",
|
||||
|
|
@ -682,6 +687,7 @@
|
|||
"map_showSharedMarkers": "Mostra i segnaposto condivisi",
|
||||
"map_lastSeenTime": "Ultimo Tempo di Visualizzazione",
|
||||
"map_sharedPin": "Condividi PIN",
|
||||
"map_sharedAt": "Condiviso",
|
||||
"map_joinRoom": "Unisciti alla stanza",
|
||||
"map_manageRepeater": "Gestisci Ripetitore",
|
||||
"mapCache_title": "Cache Mappa Offline",
|
||||
|
|
|
|||
|
|
@ -628,6 +628,11 @@
|
|||
"chat_sendGif": "GIFを送信する",
|
||||
"chat_reply": "返信",
|
||||
"chat_addReaction": "反応を追加",
|
||||
"chat_insertEmoji": "絵文字を挿入",
|
||||
"chat_shareLocation": "位置を共有する",
|
||||
"chat_stopSharingLocationConfirm": "位置情報の共有を停止しますか?",
|
||||
"chat_once": "1回",
|
||||
"chat_locationUnavailable": "位置情報が利用できません",
|
||||
"chat_me": "私",
|
||||
"emojiCategorySmileys": "笑顔の絵文字",
|
||||
"emojiCategoryGestures": "身振り、動作",
|
||||
|
|
@ -861,6 +866,7 @@
|
|||
"map_guessedLocation": "推測された場所",
|
||||
"map_lastSeenTime": "最後に確認された時間",
|
||||
"map_sharedPin": "共有パスワード",
|
||||
"map_sharedAt": "共有済み",
|
||||
"map_joinRoom": "部屋に参加する",
|
||||
"map_manageRepeater": "リピーターの管理",
|
||||
"map_tapToAdd": "ノードをクリックして、パスに追加します。",
|
||||
|
|
|
|||
|
|
@ -628,6 +628,11 @@
|
|||
"chat_sendGif": "GIF 보내기",
|
||||
"chat_reply": "답변",
|
||||
"chat_addReaction": "댓글 추가",
|
||||
"chat_insertEmoji": "이모지 삽입",
|
||||
"chat_shareLocation": "위치 공유",
|
||||
"chat_stopSharingLocationConfirm": "위치 공유를 중지하시겠습니까?",
|
||||
"chat_once": "한 번",
|
||||
"chat_locationUnavailable": "위치를 사용할 수 없습니다",
|
||||
"chat_me": "나",
|
||||
"emojiCategorySmileys": "이모티콘",
|
||||
"emojiCategoryGestures": "제스처",
|
||||
|
|
@ -861,6 +866,7 @@
|
|||
"map_guessedLocation": "추측된 위치",
|
||||
"map_lastSeenTime": "마지막으로 확인된 시간",
|
||||
"map_sharedPin": "공유 비밀번호",
|
||||
"map_sharedAt": "공유됨",
|
||||
"map_joinRoom": "방에 참여",
|
||||
"map_manageRepeater": "리피터 관리",
|
||||
"map_tapToAdd": "노드에 클릭하여 경로에 추가합니다.",
|
||||
|
|
|
|||
|
|
@ -2374,6 +2374,36 @@ abstract class AppLocalizations {
|
|||
/// **'Send GIF'**
|
||||
String get chat_sendGif;
|
||||
|
||||
/// No description provided for @chat_insertEmoji.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Insert emoji'**
|
||||
String get chat_insertEmoji;
|
||||
|
||||
/// No description provided for @chat_shareLocation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Share location'**
|
||||
String get chat_shareLocation;
|
||||
|
||||
/// No description provided for @chat_stopSharingLocationConfirm.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stop sharing location?'**
|
||||
String get chat_stopSharingLocationConfirm;
|
||||
|
||||
/// No description provided for @chat_once.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Once'**
|
||||
String get chat_once;
|
||||
|
||||
/// No description provided for @chat_locationUnavailable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Location not available'**
|
||||
String get chat_locationUnavailable;
|
||||
|
||||
/// No description provided for @chat_reply.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3124,6 +3154,12 @@ abstract class AppLocalizations {
|
|||
/// **'Shared pin'**
|
||||
String get map_sharedPin;
|
||||
|
||||
/// No description provided for @map_sharedAt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Shared'**
|
||||
String get map_sharedAt;
|
||||
|
||||
/// No description provided for @map_joinRoom.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1288,6 +1288,22 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Изпрати GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Вмъкнете емоджи';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Споделете местоположение';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm =>
|
||||
'Да спра ли споделянето на местоположение?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Веднъж';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Местоположението не е налично';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Отговори';
|
||||
|
||||
|
|
@ -1723,6 +1739,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Споделено копие';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Споделено';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Присъедини се към стаята';
|
||||
|
||||
|
|
|
|||
|
|
@ -1287,6 +1287,21 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'GIF senden';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Emoji einfügen';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Standort teilen';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm => 'Standortfreigabe beenden?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Einmal';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Standort nicht verfügbar';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Beantworten';
|
||||
|
||||
|
|
@ -1720,6 +1735,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Gemeinsames Passwort';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Geteilt';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Beitreten Sie dem Raum';
|
||||
|
||||
|
|
|
|||
|
|
@ -1262,6 +1262,21 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Send GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Insert emoji';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Share location';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm => 'Stop sharing location?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Once';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Location not available';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Reply';
|
||||
|
||||
|
|
@ -1689,6 +1704,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Shared pin';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Join Room';
|
||||
|
||||
|
|
|
|||
|
|
@ -1287,6 +1287,22 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Enviar GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Insertar emoji';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Compartir ubicación';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm =>
|
||||
'¿Dejar de compartir la ubicación?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Una vez';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Ubicación no disponible';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Responder';
|
||||
|
||||
|
|
@ -1719,6 +1735,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Pin compartido';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Compartido';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Únete a la sala';
|
||||
|
||||
|
|
|
|||
|
|
@ -1292,6 +1292,22 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Envoyer GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Insérer un emoji';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Partager la localisation';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm =>
|
||||
'Arrêter le partage de position ?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Une fois';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Localisation non disponible';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Répondre';
|
||||
|
||||
|
|
@ -1729,6 +1745,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Clé partagée';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Partagé';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Rejoindre le room server';
|
||||
|
||||
|
|
|
|||
|
|
@ -1295,6 +1295,21 @@ class AppLocalizationsHu extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Küldj GIF-ot';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Emoji beszúrása';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Helyszín megosztása';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm => 'Leállítja a helymegosztást?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Egyszer';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'A helyszín nem elérhető';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Válasz';
|
||||
|
||||
|
|
@ -1732,6 +1747,9 @@ class AppLocalizationsHu extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Gemeinsames PIN-kód';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Megosztva';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Csatlakozás a szobához';
|
||||
|
||||
|
|
|
|||
|
|
@ -1288,6 +1288,22 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Invia GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Inserisci emoji';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Condividi posizione';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm =>
|
||||
'Interrompere la condivisione della posizione?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Una volta';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Posizione non disponibile';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Rispondi';
|
||||
|
||||
|
|
@ -1720,6 +1736,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Condividi PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Condiviso';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Unisciti alla stanza';
|
||||
|
||||
|
|
|
|||
|
|
@ -1228,6 +1228,21 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'GIFを送信する';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => '絵文字を挿入';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => '位置を共有する';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm => '位置情報の共有を停止しますか?';
|
||||
|
||||
@override
|
||||
String get chat_once => '1回';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => '位置情報が利用できません';
|
||||
|
||||
@override
|
||||
String get chat_reply => '返信';
|
||||
|
||||
|
|
@ -1647,6 +1662,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => '共有パスワード';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => '共有済み';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => '部屋に参加する';
|
||||
|
||||
|
|
|
|||
|
|
@ -1223,6 +1223,21 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'GIF 보내기';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => '이모지 삽입';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => '위치 공유';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm => '위치 공유를 중지하시겠습니까?';
|
||||
|
||||
@override
|
||||
String get chat_once => '한 번';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => '위치를 사용할 수 없습니다';
|
||||
|
||||
@override
|
||||
String get chat_reply => '답변';
|
||||
|
||||
|
|
@ -1643,6 +1658,9 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => '공유 비밀번호';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => '공유됨';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => '방에 참여';
|
||||
|
||||
|
|
|
|||
|
|
@ -1276,6 +1276,21 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'GIF verzenden';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Emoji invoegen';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Locatie delen';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm => 'Locatie delen stoppen?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Eenmalig';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Locatie niet beschikbaar';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Reageren';
|
||||
|
||||
|
|
@ -1708,6 +1723,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Gedeelde pin';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Gedeeld';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Kamer Toetreden';
|
||||
|
||||
|
|
|
|||
|
|
@ -1297,6 +1297,22 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Wyślij GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Wstaw emoji';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Udostępnij lokalizację';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm =>
|
||||
'Zatrzymać udostępnianie lokalizacji?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Raz';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Lokalizacja niedostępna';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Odpowiedz';
|
||||
|
||||
|
|
@ -1732,6 +1748,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Udostępniona pinezka';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Udostępnione';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Dołącz do pokoju';
|
||||
|
||||
|
|
|
|||
|
|
@ -1287,6 +1287,22 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Enviar GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Inserir emoji';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Compartilhar local';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm =>
|
||||
'Parar de compartilhar a localização?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Uma vez';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Localização indisponível';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Responder';
|
||||
|
||||
|
|
@ -1720,6 +1736,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Pin compartilhado';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Compartilhado';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Junte-se à Sala';
|
||||
|
||||
|
|
|
|||
|
|
@ -1287,6 +1287,22 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Отправить GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Вставить эмодзи';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Поделиться местоположением';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm =>
|
||||
'Остановить передачу местоположения?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Один раз';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Местоположение недоступно';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Ответить';
|
||||
|
||||
|
|
@ -1723,6 +1739,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Общая метка';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Поделено';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Присоединиться к комнате';
|
||||
|
||||
|
|
|
|||
|
|
@ -1275,6 +1275,21 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Odoslať GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Vložiť emoji';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Zdieľať polohu';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm => 'Zastaviť zdieľanie polohy?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Raz';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Poloha nie je k dispozícii';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Odpovedať';
|
||||
|
||||
|
|
@ -1709,6 +1724,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Zdieľaný PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Zdieľané';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Pripojiť miestnosť';
|
||||
|
||||
|
|
|
|||
|
|
@ -1274,6 +1274,21 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Pošlji GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Vstavi emoji';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Deli lokacijo';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm => 'Ustavim deljenje lokacije?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Enkrat';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Lokacija ni na voljo';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Odgovori';
|
||||
|
||||
|
|
@ -1704,6 +1719,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Deljeno naslovno geslo';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Deljeno';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Pridružiti sobo';
|
||||
|
||||
|
|
|
|||
|
|
@ -1268,6 +1268,21 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Skicka GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Infoga emoji';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Dela plats';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm => 'Sluta dela plats?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'En gång';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Plats inte tillgänglig';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Svara';
|
||||
|
||||
|
|
@ -1698,6 +1713,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Delad PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Delad';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Gå med i rum';
|
||||
|
||||
|
|
|
|||
|
|
@ -1280,6 +1280,22 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => 'Надіслати GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => 'Вставити емодзі';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => 'Поділитися місцезнаходженням';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm =>
|
||||
'Зупинити поширення місцезнаходження?';
|
||||
|
||||
@override
|
||||
String get chat_once => 'Один раз';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => 'Місцезнаходження недоступне';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Відповісти';
|
||||
|
||||
|
|
@ -1718,6 +1734,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => 'Спільний пін';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Поділено';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Приєднатися до кімнати';
|
||||
|
||||
|
|
|
|||
|
|
@ -1210,6 +1210,21 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
String get chat_sendGif => '发送 GIF';
|
||||
|
||||
@override
|
||||
String get chat_insertEmoji => '插入表情';
|
||||
|
||||
@override
|
||||
String get chat_shareLocation => '分享位置';
|
||||
|
||||
@override
|
||||
String get chat_stopSharingLocationConfirm => '停止共享位置?';
|
||||
|
||||
@override
|
||||
String get chat_once => '一次';
|
||||
|
||||
@override
|
||||
String get chat_locationUnavailable => '位置不可用';
|
||||
|
||||
@override
|
||||
String get chat_reply => '回复';
|
||||
|
||||
|
|
@ -1615,6 +1630,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
String get map_sharedPin => '共享标记';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => '已分享';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => '加入房间';
|
||||
|
||||
|
|
|
|||
|
|
@ -454,6 +454,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "GIF verzenden",
|
||||
"chat_insertEmoji": "Emoji invoegen",
|
||||
"chat_shareLocation": "Locatie delen",
|
||||
"chat_stopSharingLocationConfirm": "Locatie delen stoppen?",
|
||||
"chat_once": "Eenmalig",
|
||||
"chat_locationUnavailable": "Locatie niet beschikbaar",
|
||||
"chat_reply": "Reageren",
|
||||
"chat_addReaction": "Reactie toevoegen",
|
||||
"chat_me": "Mijn",
|
||||
|
|
@ -682,6 +687,7 @@
|
|||
"map_showSharedMarkers": "Toon gedeelde markeringen",
|
||||
"map_lastSeenTime": "Laatste Bekeken Tijd",
|
||||
"map_sharedPin": "Gedeelde pin",
|
||||
"map_sharedAt": "Gedeeld",
|
||||
"map_joinRoom": "Kamer Toetreden",
|
||||
"map_manageRepeater": "Beheer Repeater",
|
||||
"mapCache_title": "Offline Kaarten Cache",
|
||||
|
|
|
|||
|
|
@ -464,6 +464,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "Wyślij GIF",
|
||||
"chat_insertEmoji": "Wstaw emoji",
|
||||
"chat_shareLocation": "Udostępnij lokalizację",
|
||||
"chat_stopSharingLocationConfirm": "Zatrzymać udostępnianie lokalizacji?",
|
||||
"chat_once": "Raz",
|
||||
"chat_locationUnavailable": "Lokalizacja niedostępna",
|
||||
"chat_reply": "Odpowiedz",
|
||||
"chat_addReaction": "Dodaj Reakcję",
|
||||
"chat_me": "Ja",
|
||||
|
|
@ -692,6 +697,7 @@
|
|||
"map_showSharedMarkers": "Pokaż udostępnione znaczniki.",
|
||||
"map_lastSeenTime": "Ostatni raz widziany",
|
||||
"map_sharedPin": "Udostępniona pinezka",
|
||||
"map_sharedAt": "Udostępnione",
|
||||
"map_joinRoom": "Dołącz do pokoju",
|
||||
"map_manageRepeater": "Zarządzaj przekaźnikiem",
|
||||
"mapCache_title": "Pamięć podręczna map offline",
|
||||
|
|
|
|||
|
|
@ -454,6 +454,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "Enviar GIF",
|
||||
"chat_insertEmoji": "Inserir emoji",
|
||||
"chat_shareLocation": "Compartilhar local",
|
||||
"chat_stopSharingLocationConfirm": "Parar de compartilhar a localização?",
|
||||
"chat_once": "Uma vez",
|
||||
"chat_locationUnavailable": "Localização indisponível",
|
||||
"chat_reply": "Responder",
|
||||
"chat_addReaction": "Adicionar Reação",
|
||||
"chat_me": "Eu",
|
||||
|
|
@ -682,6 +687,7 @@
|
|||
"map_showSharedMarkers": "Mostrar marcadores compartilhados",
|
||||
"map_lastSeenTime": "Último Tempo de Visualização",
|
||||
"map_sharedPin": "Pin compartilhado",
|
||||
"map_sharedAt": "Compartilhado",
|
||||
"map_joinRoom": "Junte-se à Sala",
|
||||
"map_manageRepeater": "Gerenciar Repetidor",
|
||||
"mapCache_title": "Cache de Mapa Offline",
|
||||
|
|
|
|||
|
|
@ -285,6 +285,11 @@
|
|||
"chat_retryingMessage": "Повтор отправки сообщения",
|
||||
"chat_retryCount": "Попытка {current}/{max}",
|
||||
"chat_sendGif": "Отправить GIF",
|
||||
"chat_insertEmoji": "Вставить эмодзи",
|
||||
"chat_shareLocation": "Поделиться местоположением",
|
||||
"chat_stopSharingLocationConfirm": "Остановить передачу местоположения?",
|
||||
"chat_once": "Один раз",
|
||||
"chat_locationUnavailable": "Местоположение недоступно",
|
||||
"chat_reply": "Ответить",
|
||||
"chat_addReaction": "Добавить реакцию",
|
||||
"chat_me": "Я",
|
||||
|
|
@ -397,6 +402,7 @@
|
|||
"map_showSharedMarkers": "Показывать общие метки",
|
||||
"map_lastSeenTime": "Время последнего появления",
|
||||
"map_sharedPin": "Общая метка",
|
||||
"map_sharedAt": "Поделено",
|
||||
"map_joinRoom": "Присоединиться к комнате",
|
||||
"map_manageRepeater": "Управление репитером",
|
||||
"mapCache_title": "Кэш офлайн-карты",
|
||||
|
|
|
|||
|
|
@ -454,6 +454,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "Odoslať GIF",
|
||||
"chat_insertEmoji": "Vložiť emoji",
|
||||
"chat_shareLocation": "Zdieľať polohu",
|
||||
"chat_stopSharingLocationConfirm": "Zastaviť zdieľanie polohy?",
|
||||
"chat_once": "Raz",
|
||||
"chat_locationUnavailable": "Poloha nie je k dispozícii",
|
||||
"chat_reply": "Odpovedať",
|
||||
"chat_addReaction": "Pridať Reakciu",
|
||||
"chat_me": "Mne",
|
||||
|
|
@ -682,6 +687,7 @@
|
|||
"map_showSharedMarkers": "Zobraziť zdieľané značky",
|
||||
"map_lastSeenTime": "Posledný čas sledovania",
|
||||
"map_sharedPin": "Zdieľaný PIN",
|
||||
"map_sharedAt": "Zdieľané",
|
||||
"map_joinRoom": "Pripojiť miestnosť",
|
||||
"map_manageRepeater": "Spravovať Opakovanie",
|
||||
"mapCache_title": "Offline Mapa Pamäť",
|
||||
|
|
|
|||
|
|
@ -454,6 +454,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "Pošlji GIF",
|
||||
"chat_insertEmoji": "Vstavi emoji",
|
||||
"chat_shareLocation": "Deli lokacijo",
|
||||
"chat_stopSharingLocationConfirm": "Ustavim deljenje lokacije?",
|
||||
"chat_once": "Enkrat",
|
||||
"chat_locationUnavailable": "Lokacija ni na voljo",
|
||||
"chat_reply": "Odgovori",
|
||||
"chat_addReaction": "Dodaj reakcijo",
|
||||
"chat_me": "jaz",
|
||||
|
|
@ -682,6 +687,7 @@
|
|||
"map_showSharedMarkers": "Pokaži skupno označenja",
|
||||
"map_lastSeenTime": "Datum zadnjega vpogleda",
|
||||
"map_sharedPin": "Deljeno naslovno geslo",
|
||||
"map_sharedAt": "Deljeno",
|
||||
"map_joinRoom": "Pridružiti sobo",
|
||||
"map_manageRepeater": "Upravljajte Ponovitve",
|
||||
"mapCache_title": "Omrezni predpomnilnik zemljeških zemljejevskih slik",
|
||||
|
|
|
|||
|
|
@ -454,6 +454,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "Skicka GIF",
|
||||
"chat_insertEmoji": "Infoga emoji",
|
||||
"chat_shareLocation": "Dela plats",
|
||||
"chat_stopSharingLocationConfirm": "Sluta dela plats?",
|
||||
"chat_once": "En gång",
|
||||
"chat_locationUnavailable": "Plats inte tillgänglig",
|
||||
"chat_reply": "Svara",
|
||||
"chat_addReaction": "Lägg till reaktion",
|
||||
"chat_me": "Mig",
|
||||
|
|
@ -682,6 +687,7 @@
|
|||
"map_showSharedMarkers": "Visa delade markörer",
|
||||
"map_lastSeenTime": "Senaste Visats Tid",
|
||||
"map_sharedPin": "Delad PIN",
|
||||
"map_sharedAt": "Delad",
|
||||
"map_joinRoom": "Gå med i rum",
|
||||
"map_manageRepeater": "Hantera Upprepare",
|
||||
"mapCache_title": "Offline Kartcache",
|
||||
|
|
|
|||
|
|
@ -455,6 +455,11 @@
|
|||
}
|
||||
},
|
||||
"chat_sendGif": "Надіслати GIF",
|
||||
"chat_insertEmoji": "Вставити емодзі",
|
||||
"chat_shareLocation": "Поділитися місцезнаходженням",
|
||||
"chat_stopSharingLocationConfirm": "Зупинити поширення місцезнаходження?",
|
||||
"chat_once": "Один раз",
|
||||
"chat_locationUnavailable": "Місцезнаходження недоступне",
|
||||
"chat_reply": "Відповісти",
|
||||
"chat_addReaction": "Додати реакцію",
|
||||
"chat_me": "Я",
|
||||
|
|
@ -683,6 +688,7 @@
|
|||
"map_showSharedMarkers": "Показувати спільні маркери",
|
||||
"map_lastSeenTime": "Час останньої активності",
|
||||
"map_sharedPin": "Спільний пін",
|
||||
"map_sharedAt": "Поділено",
|
||||
"map_joinRoom": "Приєднатися до кімнати",
|
||||
"map_manageRepeater": "Керувати ретранслятором",
|
||||
"mapCache_title": "Офлайн-кеш карти",
|
||||
|
|
|
|||
|
|
@ -483,6 +483,11 @@
|
|||
"chat_sendGif": "发送 GIF",
|
||||
"chat_reply": "回复",
|
||||
"chat_addReaction": "添加表情",
|
||||
"chat_insertEmoji": "插入表情",
|
||||
"chat_shareLocation": "分享位置",
|
||||
"chat_stopSharingLocationConfirm": "停止共享位置?",
|
||||
"chat_once": "一次",
|
||||
"chat_locationUnavailable": "位置不可用",
|
||||
"chat_me": "我",
|
||||
"emojiCategorySmileys": "表情",
|
||||
"emojiCategoryGestures": "手势",
|
||||
|
|
@ -709,6 +714,7 @@
|
|||
"map_showSharedMarkers": "显示共享标记",
|
||||
"map_lastSeenTime": "最后在线时间",
|
||||
"map_sharedPin": "共享标记",
|
||||
"map_sharedAt": "已分享",
|
||||
"map_joinRoom": "加入房间",
|
||||
"map_manageRepeater": "管理转发节点",
|
||||
"mapCache_title": "离线地图缓存",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import '../connector/meshcore_protocol.dart';
|
|||
import '../helpers/gif_helper.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';
|
||||
|
|
@ -43,6 +44,8 @@ class ChannelChatScreen extends StatefulWidget {
|
|||
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
||||
}
|
||||
|
||||
enum _ChannelChatInputAction { sendGif, insertEmoji, shareLocation }
|
||||
|
||||
class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
final ChatScrollController _scrollController = ChatScrollController();
|
||||
|
|
@ -898,6 +901,252 @@ 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) {
|
||||
_insertTextAtCursor(emoji);
|
||||
_textFieldFocusNode.requestFocus();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _shareLocation() async {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final lat = connector.selfLatitude;
|
||||
final lon = connector.selfLongitude;
|
||||
final allowsTimedLocationSharing =
|
||||
!widget.channel.isPublicChannel && !widget.channel.name.startsWith('#');
|
||||
if (lat == null || lon == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_locationUnavailable)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (allowsTimedLocationSharing &&
|
||||
connector.locationSharingChannelIndex == widget.channel.index) {
|
||||
final stop = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.l10n.chat_shareLocation),
|
||||
content: Text(context.l10n.chat_stopSharingLocationConfirm),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: Text(context.l10n.common_ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (stop == true) connector.stopLocationSharing();
|
||||
return;
|
||||
}
|
||||
|
||||
final maxBytes = maxChannelMessageBytes(connector.selfName);
|
||||
final prefix = 'm:${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}|';
|
||||
const suffix = '|loc';
|
||||
final maxLabelBytes =
|
||||
maxBytes - utf8.encode(prefix).length - utf8.encode(suffix).length;
|
||||
|
||||
if (allowsTimedLocationSharing) {
|
||||
final rawGpsInterval =
|
||||
int.tryParse(connector.currentCustomVars?['gps_interval'] ?? '') ??
|
||||
900;
|
||||
final gpsInterval = rawGpsInterval > 0 ? rawGpsInterval : 900;
|
||||
final minIntervals = (300.0 / gpsInterval).ceil().clamp(2, 9999);
|
||||
final sliderMax = ((86400 / gpsInterval).floor() - minIntervals + 1)
|
||||
.clamp(1, 99999);
|
||||
final labelCtrl = TextEditingController(
|
||||
text: truncateToUtf8Bytes(connector.deviceDisplayName, maxLabelBytes),
|
||||
);
|
||||
int sliderVal = 0;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (dialogContext, setDialogState) {
|
||||
final actual = sliderVal == 0 ? 0 : sliderVal + minIntervals - 1;
|
||||
final duration = Duration(seconds: actual * gpsInterval);
|
||||
final durationLabel = sliderVal == 0
|
||||
? context.l10n.chat_once
|
||||
: (duration.inHours > 0
|
||||
? '${duration.inHours}h ${duration.inMinutes.remainder(60)}m'
|
||||
: '${duration.inMinutes}m');
|
||||
return AlertDialog(
|
||||
title: Text(context.l10n.chat_shareLocation),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(durationLabel),
|
||||
Slider(
|
||||
min: 0,
|
||||
max: sliderMax.toDouble(),
|
||||
divisions: sliderMax,
|
||||
value: sliderVal.toDouble(),
|
||||
onChanged: (value) =>
|
||||
setDialogState(() => sliderVal = value.round()),
|
||||
),
|
||||
if (sliderVal == 0)
|
||||
TextField(
|
||||
controller: labelCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.chat_location,
|
||||
),
|
||||
autofocus: true,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(maxLabelBytes),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: Text(context.l10n.common_ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
if (sliderVal == 0) {
|
||||
var label = labelCtrl.text.trim().replaceAll('|', '/');
|
||||
if (label.isEmpty) return;
|
||||
|
||||
final markerText =
|
||||
'm:${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}|$label|loc';
|
||||
if (utf8.encode(markerText).length > maxBytes) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
connector.sendChannelMessage(widget.channel, markerText);
|
||||
} else {
|
||||
connector.startLocationSharing(
|
||||
channelIndex: widget.channel.index,
|
||||
duration: Duration(
|
||||
seconds: (sliderVal + minIntervals - 1) * gpsInterval,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final defaultLabel = connector.deviceDisplayName;
|
||||
final controller = TextEditingController(
|
||||
text: truncateToUtf8Bytes(defaultLabel, maxLabelBytes),
|
||||
);
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: controller.text.length,
|
||||
);
|
||||
|
||||
var label = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.l10n.chat_shareLocation),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(labelText: context.l10n.chat_location),
|
||||
autofocus: true,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(maxLabelBytes),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text.trim()),
|
||||
child: Text(context.l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (label == null || label.isEmpty) return;
|
||||
label = label.replaceAll('|', '/');
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final markerText =
|
||||
'm:${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}|$label|loc';
|
||||
if (utf8.encode(markerText).length > maxBytes) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
final initial = _getFirstCharacterOrEmoji(senderName);
|
||||
final color = _getColorForName(senderName);
|
||||
|
|
@ -1007,6 +1256,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
final connector = context.watch<MeshCoreConnector>();
|
||||
final maxBytes = maxChannelMessageBytes(connector.selfName);
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final smazEncoder = connector.isChannelSmazEnabled(widget.channel.index)
|
||||
? Smaz.encodeIfSmaller
|
||||
: null;
|
||||
final gpsEnabled = connector.currentCustomVars?['gps'] == '1';
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
|
@ -1019,6 +1272,29 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
return _buildReplyBanner(textScale);
|
||||
},
|
||||
),
|
||||
if (connector.locationSharingChannelIndex == widget.channel.index)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 4, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.my_location, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.l10n.chat_shareLocation,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: connector.stopLocationSharing,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -1033,10 +1309,54 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.gif_box),
|
||||
onPressed: () => _showGifPicker(context),
|
||||
tooltip: context.l10n.chat_sendGif,
|
||||
PopupMenuButton<_ChannelChatInputAction>(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
tooltip: context.l10n.common_add,
|
||||
requestFocus: false,
|
||||
position: PopupMenuPosition.over,
|
||||
offset: const Offset(0, -180),
|
||||
onSelected: (action) {
|
||||
if (action == _ChannelChatInputAction.sendGif) {
|
||||
_showGifPicker(context);
|
||||
} else if (action == _ChannelChatInputAction.insertEmoji) {
|
||||
_showEmojiPickerForComposer(context);
|
||||
} else if (action == _ChannelChatInputAction.shareLocation) {
|
||||
_shareLocation();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: _ChannelChatInputAction.shareLocation,
|
||||
enabled: gpsEnabled,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.chat_shareLocation),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _ChannelChatInputAction.sendGif,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.gif_box, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.chat_sendGif),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _ChannelChatInputAction.insertEmoji,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.emoji_emotions, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.chat_insertEmoji),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (settings.translationEnabled)
|
||||
MessageTranslationButton(
|
||||
|
|
@ -1098,7 +1418,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
controller: _textController,
|
||||
focusNode: _textFieldFocusNode,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(maxBytes),
|
||||
Utf8LengthLimitingTextInputFormatter(
|
||||
maxBytes,
|
||||
encoder: smazEncoder,
|
||||
),
|
||||
],
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
|
|
@ -1193,7 +1516,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
}
|
||||
|
||||
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))),
|
||||
);
|
||||
|
|
@ -1299,6 +1626,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => EmojiPicker(
|
||||
title: context.l10n.chat_addReaction,
|
||||
onEmojiSelected: (emoji) {
|
||||
_sendReaction(message, emoji);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import '../helpers/chat_scroll_controller.dart';
|
|||
import '../helpers/gif_helper.dart';
|
||||
import '../helpers/path_helper.dart';
|
||||
import '../helpers/utf8_length_limiter.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/message.dart';
|
||||
|
|
@ -45,6 +46,8 @@ import '../utils/app_logger.dart';
|
|||
import '../l10n/l10n.dart';
|
||||
import 'telemetry_screen.dart';
|
||||
|
||||
enum _ChatInputAction { sendGif, insertEmoji, shareLocation }
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
|
||||
|
|
@ -500,103 +503,343 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
final maxBytes = maxContactMessageBytes();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
border: Border(top: BorderSide(color: Theme.of(context).dividerColor)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.gif_box),
|
||||
onPressed: () => _showGifPicker(context),
|
||||
tooltip: context.l10n.chat_sendGif,
|
||||
final smazEncoder =
|
||||
connector.isContactSmazEnabled(widget.contact.publicKeyHex)
|
||||
? Smaz.encodeIfSmaller
|
||||
: null;
|
||||
final gpsEnabled = connector.currentCustomVars?['gps'] == '1';
|
||||
final isRoomChat = _resolveContact(connector).type == advTypeRoom;
|
||||
final sharingHere =
|
||||
connector.locationSharingContactKey == widget.contact.publicKeyHex;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (sharingHere)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 4, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.my_location, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.l10n.chat_shareLocation,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: connector.stopLocationSharing,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (settings.translationEnabled)
|
||||
MessageTranslationButton(
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
languageCode: settings.translationTargetLanguageCode,
|
||||
onPressed: _showTranslationOptions,
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
builder: (context, value, child) {
|
||||
final gifId = GifHelper.parseGif(value.text);
|
||||
if (gifId != null) {
|
||||
return Focus(
|
||||
autofocus: true,
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyDownEvent &&
|
||||
(event.logicalKey == LogicalKeyboardKey.enter ||
|
||||
event.logicalKey ==
|
||||
LogicalKeyboardKey.numpadEnter)) {
|
||||
_sendMessage(connector);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
PopupMenuButton<_ChatInputAction>(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
tooltip: context.l10n.common_add,
|
||||
requestFocus: false,
|
||||
position: PopupMenuPosition.over,
|
||||
offset: const Offset(0, -180),
|
||||
onSelected: (action) {
|
||||
if (action == _ChatInputAction.sendGif) {
|
||||
_showGifPicker(context);
|
||||
} else if (action == _ChatInputAction.insertEmoji) {
|
||||
_showEmojiPicker(context);
|
||||
} else if (action == _ChatInputAction.shareLocation) {
|
||||
_shareLocation(context.read<MeshCoreConnector>());
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: _ChatInputAction.shareLocation,
|
||||
enabled: gpsEnabled && !isRoomChat,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor:
|
||||
colorScheme.surfaceContainerHighest,
|
||||
fallbackTextColor: colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
maxSize: 160,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.my_location, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
_textController.clear();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
},
|
||||
),
|
||||
Text(context.l10n.chat_shareLocation),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TextField(
|
||||
controller: _textController,
|
||||
focusNode: _textFieldFocusNode,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(maxBytes),
|
||||
],
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _ChatInputAction.sendGif,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.gif_box, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.chat_sendGif),
|
||||
],
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _sendMessage(connector),
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _ChatInputAction.insertEmoji,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.emoji_emotions, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.chat_insertEmoji),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (settings.translationEnabled)
|
||||
MessageTranslationButton(
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
languageCode: settings.translationTargetLanguageCode,
|
||||
onPressed: _showTranslationOptions,
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
builder: (context, value, child) {
|
||||
final gifId = GifHelper.parseGif(value.text);
|
||||
if (gifId != null) {
|
||||
return Focus(
|
||||
autofocus: true,
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyDownEvent &&
|
||||
(event.logicalKey == LogicalKeyboardKey.enter ||
|
||||
event.logicalKey ==
|
||||
LogicalKeyboardKey.numpadEnter)) {
|
||||
_sendMessage(connector);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor:
|
||||
colorScheme.surfaceContainerHighest,
|
||||
fallbackTextColor: colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
maxSize: 160,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
_textController.clear();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TextField(
|
||||
controller: _textController,
|
||||
focusNode: _textFieldFocusNode,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(
|
||||
maxBytes,
|
||||
encoder: smazEncoder,
|
||||
),
|
||||
],
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _sendMessage(connector),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton.filled(
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: () => _sendMessage(connector),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton.filled(
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: () => _sendMessage(connector),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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 _showEmojiPicker(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => EmojiPicker(
|
||||
title: context.l10n.chat_insertEmoji,
|
||||
onEmojiSelected: (emoji) {
|
||||
_insertTextAtCursor(emoji);
|
||||
_textFieldFocusNode.requestFocus();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _shareLocation(MeshCoreConnector connector) async {
|
||||
if (_resolveContact(connector).type == advTypeRoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
final lat = connector.selfLatitude;
|
||||
final lon = connector.selfLongitude;
|
||||
if (lat == null || lon == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_locationUnavailable)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (connector.locationSharingContactKey == widget.contact.publicKeyHex) {
|
||||
final stop = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(context.l10n.chat_shareLocation),
|
||||
content: Text(context.l10n.chat_stopSharingLocationConfirm),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(context.l10n.common_ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (stop == true) connector.stopLocationSharing();
|
||||
return;
|
||||
}
|
||||
final maxBytes = maxContactMessageBytes();
|
||||
final prefix = 'm:${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}|';
|
||||
const suffix = '|loc';
|
||||
final maxLabelBytes =
|
||||
maxBytes - utf8.encode(prefix).length - utf8.encode(suffix).length;
|
||||
final rawGpsInterval =
|
||||
int.tryParse(connector.currentCustomVars?['gps_interval'] ?? '') ?? 900;
|
||||
final gpsInterval = rawGpsInterval > 0 ? rawGpsInterval : 900;
|
||||
final minIntervals = (300.0 / gpsInterval).ceil().clamp(2, 9999);
|
||||
final sliderMax = ((86400 / gpsInterval).floor() - minIntervals + 1).clamp(
|
||||
1,
|
||||
99999,
|
||||
);
|
||||
final labelCtrl = TextEditingController(
|
||||
text: truncateToUtf8Bytes(connector.deviceDisplayName, maxLabelBytes),
|
||||
);
|
||||
int sliderVal = 0;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx2, setS) {
|
||||
final actual = sliderVal == 0 ? 0 : sliderVal + minIntervals - 1;
|
||||
final dur = Duration(seconds: actual * gpsInterval);
|
||||
final dLabel = sliderVal == 0
|
||||
? context.l10n.chat_once
|
||||
: (dur.inHours > 0
|
||||
? '${dur.inHours}h ${dur.inMinutes.remainder(60)}m'
|
||||
: '${dur.inMinutes}m');
|
||||
return AlertDialog(
|
||||
title: Text(context.l10n.chat_shareLocation),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(dLabel),
|
||||
Slider(
|
||||
min: 0,
|
||||
max: sliderMax.toDouble(),
|
||||
divisions: sliderMax,
|
||||
value: sliderVal.toDouble(),
|
||||
onChanged: (v) => setS(() => sliderVal = v.round()),
|
||||
),
|
||||
if (sliderVal == 0)
|
||||
TextField(
|
||||
controller: labelCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.chat_location,
|
||||
),
|
||||
autofocus: true,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(maxLabelBytes),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(context.l10n.common_ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
if (sliderVal == 0) {
|
||||
var label = labelCtrl.text.trim().replaceAll('|', '/');
|
||||
if (label.isEmpty) return;
|
||||
final markerText =
|
||||
'm:${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}|$label|loc';
|
||||
if (utf8.encode(markerText).length > maxBytes) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
connector.sendMessage(_resolveContact(connector), markerText);
|
||||
} else {
|
||||
connector.startLocationSharing(
|
||||
contactKey: widget.contact.publicKeyHex,
|
||||
duration: Duration(
|
||||
seconds: (sliderVal + minIntervals - 1) * gpsInterval,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showGifPicker(BuildContext context) {
|
||||
|
|
@ -667,7 +910,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
}
|
||||
}
|
||||
final maxBytes = maxContactMessageBytes();
|
||||
if (utf8.encode(outgoingText).length > maxBytes) {
|
||||
final outboundText = connector.prepareContactOutboundText(
|
||||
widget.contact,
|
||||
outgoingText,
|
||||
);
|
||||
if (utf8.encode(outboundText).length > maxBytes) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
|
||||
);
|
||||
|
|
@ -1427,7 +1674,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
title: Text(context.l10n.chat_addReaction),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_showEmojiPicker(message, contact);
|
||||
_showReactionEmojiPicker(message, contact);
|
||||
},
|
||||
),
|
||||
if (PlatformInfo.isDesktop)
|
||||
|
|
@ -1509,11 +1756,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage)));
|
||||
}
|
||||
|
||||
void _showEmojiPicker(Message message, Contact senderContact) {
|
||||
void _showReactionEmojiPicker(Message message, Contact senderContact) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => EmojiPicker(
|
||||
title: context.l10n.chat_addReaction,
|
||||
onEmojiSelected: (emoji) {
|
||||
_sendReaction(message, senderContact, emoji);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1222,7 +1222,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||
}
|
||||
|
||||
List<_SharedMarker> _collectSharedMarkers(MeshCoreConnector connector) {
|
||||
final markers = <_SharedMarker>[];
|
||||
final markersByKey = <String, _SharedMarker>{};
|
||||
final selfName = connector.selfName ?? 'Me';
|
||||
|
||||
for (final contact in connector.contacts) {
|
||||
|
|
@ -1231,23 +1231,28 @@ class _MapScreenState extends State<MapScreen> {
|
|||
final payload = _parseMarkerText(message.text);
|
||||
if (payload == null) continue;
|
||||
final fromName = message.isOutgoing ? selfName : contact.name;
|
||||
final id = _buildMarkerId(
|
||||
final key = _buildSharedMarkerKey(
|
||||
sourceId: contact.publicKeyHex,
|
||||
label: payload.label,
|
||||
fromName: fromName,
|
||||
flags: payload.flags,
|
||||
isChannel: false,
|
||||
);
|
||||
final marker = _SharedMarker(
|
||||
id: key,
|
||||
position: payload.position,
|
||||
label: payload.label,
|
||||
flags: payload.flags,
|
||||
fromName: fromName,
|
||||
sourceLabel: contact.name,
|
||||
timestamp: message.timestamp,
|
||||
text: message.text,
|
||||
);
|
||||
markers.add(
|
||||
_SharedMarker(
|
||||
id: id,
|
||||
position: payload.position,
|
||||
label: payload.label,
|
||||
flags: payload.flags,
|
||||
fromName: fromName,
|
||||
sourceLabel: contact.name,
|
||||
isChannel: false,
|
||||
isPublicChannel: false,
|
||||
),
|
||||
isChannel: false,
|
||||
isPublicChannel: false,
|
||||
);
|
||||
final existing = markersByKey[key];
|
||||
if (existing == null || marker.timestamp.isAfter(existing.timestamp)) {
|
||||
markersByKey[key] = marker;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1257,28 +1262,35 @@ class _MapScreenState extends State<MapScreen> {
|
|||
for (final message in messages) {
|
||||
final payload = _parseMarkerText(message.text);
|
||||
if (payload == null) continue;
|
||||
final id = _buildMarkerId(
|
||||
final key = _buildSharedMarkerKey(
|
||||
sourceId: 'channel:${channel.index}',
|
||||
label: payload.label,
|
||||
fromName: message.senderName,
|
||||
flags: payload.flags,
|
||||
isChannel: true,
|
||||
);
|
||||
final marker = _SharedMarker(
|
||||
id: key,
|
||||
position: payload.position,
|
||||
label: payload.label,
|
||||
flags: payload.flags,
|
||||
fromName: message.senderName,
|
||||
sourceLabel: channel.name.isEmpty
|
||||
? 'Channel ${channel.index}'
|
||||
: channel.name,
|
||||
timestamp: message.timestamp,
|
||||
text: message.text,
|
||||
);
|
||||
markers.add(
|
||||
_SharedMarker(
|
||||
id: id,
|
||||
position: payload.position,
|
||||
label: payload.label,
|
||||
flags: payload.flags,
|
||||
fromName: message.senderName,
|
||||
sourceLabel: channel.name.isEmpty
|
||||
? 'Channel ${channel.index}'
|
||||
: channel.name,
|
||||
isChannel: true,
|
||||
isPublicChannel: isPublic,
|
||||
),
|
||||
isChannel: true,
|
||||
isPublicChannel: isPublic,
|
||||
);
|
||||
final existing = markersByKey[key];
|
||||
if (existing == null || marker.timestamp.isAfter(existing.timestamp)) {
|
||||
markersByKey[key] = marker;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final markers = markersByKey.values.toList()
|
||||
..sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||
return markers;
|
||||
}
|
||||
|
||||
|
|
@ -1298,17 +1310,23 @@ class _MapScreenState extends State<MapScreen> {
|
|||
final flags = parts.length > 2 ? parts[2].trim() : '';
|
||||
return _MarkerPayload(
|
||||
position: LatLng(lat, lon),
|
||||
label: label.isEmpty ? context.l10n.map_sharedPin : label,
|
||||
label: label,
|
||||
flags: flags,
|
||||
);
|
||||
}
|
||||
|
||||
String _buildMarkerId({
|
||||
String _buildSharedMarkerKey({
|
||||
required String sourceId,
|
||||
required DateTime timestamp,
|
||||
required String text,
|
||||
required String label,
|
||||
required String fromName,
|
||||
required String flags,
|
||||
required bool isChannel,
|
||||
}) {
|
||||
return '$sourceId|${timestamp.millisecondsSinceEpoch}|$text';
|
||||
final normalizedLabel = label.trim().toLowerCase();
|
||||
final normalizedFrom = fromName.trim().toLowerCase();
|
||||
final normalizedFlags = flags.trim().toLowerCase();
|
||||
final scope = isChannel ? 'ch' : 'dm';
|
||||
return '$scope|$sourceId|$normalizedFrom|$normalizedLabel|$normalizedFlags';
|
||||
}
|
||||
|
||||
Marker _buildSharedMarker(_SharedMarker marker) {
|
||||
|
|
@ -1521,13 +1539,19 @@ class _MapScreenState extends State<MapScreen> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(marker.label),
|
||||
title: Text(
|
||||
marker.label.isEmpty ? context.l10n.map_sharedPin : marker.label,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(context.l10n.map_from, marker.fromName),
|
||||
_buildInfoRow(context.l10n.map_source, marker.sourceLabel),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_sharedAt,
|
||||
_formatLastSeen(marker.timestamp),
|
||||
),
|
||||
_buildInfoRow(
|
||||
'Location',
|
||||
'${marker.position.latitude.toStringAsFixed(6)}, ${marker.position.longitude.toStringAsFixed(6)}',
|
||||
|
|
@ -1690,6 +1714,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(
|
||||
|
|
@ -2294,6 +2322,7 @@ class _SharedMarker {
|
|||
final String flags;
|
||||
final String fromName;
|
||||
final String sourceLabel;
|
||||
final DateTime timestamp;
|
||||
final bool isChannel;
|
||||
final bool isPublicChannel;
|
||||
|
||||
|
|
@ -2304,6 +2333,7 @@ class _SharedMarker {
|
|||
required this.flags,
|
||||
required this.fromName,
|
||||
required this.sourceLabel,
|
||||
required this.timestamp,
|
||||
required this.isChannel,
|
||||
required this.isPublicChannel,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
final Set<String> _activeMessages = {};
|
||||
final Set<String> _resolvedMessages = {};
|
||||
final Map<String, String> _expectedHashToMessageId = {};
|
||||
final Set<String> _forceClearPathOnMaxRetry = {};
|
||||
|
||||
RetryServiceConfig? _config;
|
||||
|
||||
|
|
@ -143,6 +144,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
String? translationModelId,
|
||||
Uint8List? pathBytes,
|
||||
int? pathLength,
|
||||
bool forceClearPathOnMaxRetry = false,
|
||||
}) async {
|
||||
final messageId = const Uuid().v4();
|
||||
final resolved = resolvePathSelection(contact);
|
||||
|
|
@ -167,6 +169,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||
|
||||
_pendingMessages[messageId] = message;
|
||||
_pendingContacts[messageId] = contact;
|
||||
if (forceClearPathOnMaxRetry) {
|
||||
_forceClearPathOnMaxRetry.add(messageId);
|
||||
}
|
||||
|
||||
_config?.addMessage(contact.publicKeyHex, message);
|
||||
|
||||
|
|
@ -458,6 +463,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
_pendingMessages.remove(messageId);
|
||||
_pendingContacts.remove(messageId);
|
||||
_attemptPathHistory.remove(messageId);
|
||||
_forceClearPathOnMaxRetry.remove(messageId);
|
||||
_timeoutTimers.remove(messageId);
|
||||
_resolvedMessages.remove(messageId);
|
||||
}
|
||||
|
|
@ -519,8 +525,10 @@ class MessageRetryService extends ChangeNotifier {
|
|||
final failedMessage = message.copyWith(status: MessageStatus.failed);
|
||||
_pendingMessages[messageId] = failedMessage;
|
||||
|
||||
if (config?.appSettingsService?.settings.clearPathOnMaxRetry == true &&
|
||||
config?.clearContactPath != null) {
|
||||
final shouldClearPathOnMaxRetry =
|
||||
_forceClearPathOnMaxRetry.contains(messageId) ||
|
||||
config?.appSettingsService?.settings.clearPathOnMaxRetry == true;
|
||||
if (shouldClearPathOnMaxRetry && config?.clearContactPath != null) {
|
||||
config!.clearContactPath!(contact);
|
||||
}
|
||||
|
||||
|
|
@ -756,6 +764,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
_sendQueue.clear();
|
||||
_activeMessages.clear();
|
||||
_resolvedMessages.clear();
|
||||
_forceClearPathOnMaxRetry.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue