Skip to content

Commit 0287de1

Browse files
authored
Merge pull request #438 from ericszimmermann/ez_translate_notification
Translate Notifications
2 parents a50c0d0 + bac82dc commit 0287de1

46 files changed

Lines changed: 554 additions & 140 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

lib/connector/meshcore_connector.dart

Lines changed: 145 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,19 +1106,31 @@ class MeshCoreConnector extends ChangeNotifier {
11061106
}
11071107
}
11081108

1109-
Future<void> _translateIncomingContactMessage(
1109+
Future<TranslationResult?> translateContactMessage(
11101110
String contactKeyHex,
1111-
Message message,
1112-
) async {
1111+
Message message, {
1112+
bool manualTranslation = false,
1113+
}) async {
11131114
try {
1115+
if (message.translatedText?.trim().isNotEmpty == true ||
1116+
(!manualTranslation &&
1117+
message.translationStatus != MessageTranslationStatus.none)) {
1118+
return null;
1119+
}
11141120
final service = _translationService;
11151121
if (service == null ||
1116-
!service.shouldTranslateIncoming(
1117-
text: message.text,
1118-
isCli: message.isCli,
1119-
isOutgoing: message.isOutgoing,
1120-
)) {
1121-
return;
1122+
!(manualTranslation
1123+
? service.canTranslateIncoming(
1124+
text: message.text,
1125+
isCli: message.isCli,
1126+
isOutgoing: message.isOutgoing,
1127+
)
1128+
: service.shouldAutoTranslateIncoming(
1129+
text: message.text,
1130+
isCli: message.isCli,
1131+
isOutgoing: message.isOutgoing,
1132+
))) {
1133+
return null;
11221134
}
11231135
final targetLanguageCode = service.resolvedIncomingLanguageCode(
11241136
_appSettingsService?.settings.languageOverride,
@@ -1128,7 +1140,7 @@ class MeshCoreConnector extends ChangeNotifier {
11281140
targetLanguageCode: targetLanguageCode,
11291141
);
11301142
if (result == null) {
1131-
return;
1143+
return null;
11321144
}
11331145
final translated = result.status == MessageTranslationStatus.completed
11341146
? result.translatedText
@@ -1143,24 +1155,38 @@ class MeshCoreConnector extends ChangeNotifier {
11431155
translationModelId: result.modelId,
11441156
),
11451157
);
1158+
return result;
11461159
} catch (error) {
11471160
appLogger.warn('Translation failed for contact message: $error');
1161+
return null;
11481162
}
11491163
}
11501164

1151-
Future<void> _translateIncomingChannelMessage(
1165+
Future<TranslationResult?> translateChannelMessage(
11521166
int channelIndex,
1153-
ChannelMessage message,
1154-
) async {
1167+
ChannelMessage message, {
1168+
bool manualTranslation = false,
1169+
}) async {
11551170
try {
1171+
if (message.translatedText?.trim().isNotEmpty == true ||
1172+
(!manualTranslation &&
1173+
message.translationStatus != MessageTranslationStatus.none)) {
1174+
return null;
1175+
}
11561176
final service = _translationService;
11571177
if (service == null ||
1158-
!service.shouldTranslateIncoming(
1159-
text: message.text,
1160-
isCli: false,
1161-
isOutgoing: message.isOutgoing,
1162-
)) {
1163-
return;
1178+
!(manualTranslation
1179+
? service.canTranslateIncoming(
1180+
text: message.text,
1181+
isCli: false,
1182+
isOutgoing: message.isOutgoing,
1183+
)
1184+
: service.shouldAutoTranslateIncoming(
1185+
text: message.text,
1186+
isCli: false,
1187+
isOutgoing: message.isOutgoing,
1188+
))) {
1189+
return null;
11641190
}
11651191
final targetLanguageCode = service.resolvedIncomingLanguageCode(
11661192
_appSettingsService?.settings.languageOverride,
@@ -1170,11 +1196,16 @@ class MeshCoreConnector extends ChangeNotifier {
11701196
targetLanguageCode: targetLanguageCode,
11711197
);
11721198
if (result == null) {
1173-
return;
1199+
return null;
11741200
}
1175-
final translated = result.status == MessageTranslationStatus.completed
1201+
var translated = result.status == MessageTranslationStatus.completed
11761202
? result.translatedText
11771203
: null;
1204+
// Strip replyInfo prefix from translated text to match stored message.text
1205+
if (translated != null) {
1206+
final regex = RegExp(r'^@\[[^\]]+\]\s+', dotAll: true);
1207+
translated = translated.replaceFirst(regex, '');
1208+
}
11781209
_updateStoredChannelMessage(
11791210
channelIndex,
11801211
message.messageId,
@@ -1185,8 +1216,10 @@ class MeshCoreConnector extends ChangeNotifier {
11851216
translationModelId: result.modelId,
11861217
),
11871218
);
1219+
return result;
11881220
} catch (error) {
11891221
appLogger.warn('Translation failed for channel message: $error');
1222+
return null;
11901223
}
11911224
}
11921225

@@ -4373,6 +4406,12 @@ class MeshCoreConnector extends ChangeNotifier {
43734406
void _handleIncomingMessage(Uint8List frame) async {
43744407
if (_selfPublicKey == null) return;
43754408

4409+
// If we're syncing the queued messages, advance the queue immediately
4410+
// before any potentially long async work (like translation/notifications).
4411+
if (_isSyncingQueuedMessages) {
4412+
_handleQueuedMessageReceived();
4413+
}
4414+
43764415
var message = _parseContactMessage(frame);
43774416

43784417
// If message parsing failed due to unknown contact, refresh contacts and retry
@@ -4438,35 +4477,52 @@ class MeshCoreConnector extends ChangeNotifier {
44384477
}
44394478
}
44404479
_addMessage(message.senderKeyHex, message);
4441-
if (!message.isOutgoing) {
4442-
unawaited(
4443-
_translateIncomingContactMessage(message.senderKeyHex, message),
4444-
);
4445-
}
44464480
_maybeIncrementContactUnread(message);
44474481
notifyListeners();
44484482

4449-
// Show notification for new incoming message
4483+
// Show notification for new incoming message (run async with translation)
44504484
if (!message.isOutgoing &&
44514485
!message.isCli &&
44524486
_appSettingsService != null) {
44534487
final settings = _appSettingsService!.settings;
44544488
if (settings.notificationsEnabled && settings.notifyOnNewMessage) {
4455-
if (contact?.type == advTypeChat) {
4456-
_notificationService.showMessageNotification(
4457-
contactName: contact?.name ?? 'Unknown',
4458-
message: message.text,
4459-
contactId: message.senderKeyHex,
4460-
badgeCount: getTotalUnreadCount(),
4461-
);
4462-
} else if (contact?.type == advTypeRoom) {
4463-
_notificationService.showMessageNotification(
4464-
contactName: contact?.name ?? 'Unknown Room',
4465-
message: message.text,
4466-
contactId: message.senderKeyHex,
4467-
badgeCount: getTotalUnreadCount(),
4489+
final msg = message; // capture for closure
4490+
final c = contact; // capture contact reference
4491+
unawaited(() async {
4492+
final translationResult = await translateContactMessage(
4493+
msg.senderKeyHex,
4494+
msg,
44684495
);
4469-
}
4496+
if (c?.type == advTypeChat) {
4497+
final resolvedText =
4498+
(translationResult != null &&
4499+
translationResult.status ==
4500+
MessageTranslationStatus.completed &&
4501+
translationResult.translatedText.trim().isNotEmpty)
4502+
? translationResult.translatedText.trim()
4503+
: msg.text.trim();
4504+
await _notificationService.showMessageNotification(
4505+
contactName: c?.name ?? 'Unknown',
4506+
message: resolvedText,
4507+
contactId: msg.senderKeyHex,
4508+
badgeCount: getTotalUnreadCount(),
4509+
);
4510+
} else if (c?.type == advTypeRoom) {
4511+
final resolvedText =
4512+
(translationResult != null &&
4513+
translationResult.status ==
4514+
MessageTranslationStatus.completed &&
4515+
translationResult.translatedText.trim().isNotEmpty)
4516+
? translationResult.translatedText.trim()
4517+
: msg.text.trim();
4518+
await _notificationService.showMessageNotification(
4519+
contactName: c?.name ?? 'Unknown Room',
4520+
message: resolvedText,
4521+
contactId: msg.senderKeyHex,
4522+
badgeCount: getTotalUnreadCount(),
4523+
);
4524+
}
4525+
}());
44704526
}
44714527
}
44724528
_handleQueuedMessageReceived();
@@ -4740,6 +4796,7 @@ class MeshCoreConnector extends ChangeNotifier {
47404796
void _maybeNotifyChannelMessage(
47414797
ChannelMessage message, {
47424798
String? channelName,
4799+
TranslationResult? translationResult,
47434800
}) {
47444801
if (message.isOutgoing || _appSettingsService == null) return;
47454802
final channelIndex = message.channelIndex;
@@ -4753,16 +4810,30 @@ class MeshCoreConnector extends ChangeNotifier {
47534810
final label = channelName ?? _channelDisplayName(channelIndex);
47544811
if (_appSettingsService!.isChannelMuted(label)) return;
47554812

4756-
_notificationService.showChannelMessageNotification(
4757-
channelName: label,
4758-
senderName: message.senderName,
4759-
message: message.text,
4760-
channelIndex: channelIndex,
4761-
badgeCount: getTotalUnreadCount(),
4762-
);
4813+
// Reuse translation result only if completed and non-empty; else use original text
4814+
final resolvedText =
4815+
(translationResult != null &&
4816+
translationResult.status == MessageTranslationStatus.completed &&
4817+
translationResult.translatedText.trim().isNotEmpty)
4818+
? translationResult.translatedText.trim()
4819+
: message.text.trim();
4820+
unawaited(() async {
4821+
await _notificationService.showChannelMessageNotification(
4822+
channelName: label,
4823+
senderName: message.senderName,
4824+
message: resolvedText,
4825+
channelIndex: message.channelIndex,
4826+
badgeCount: getTotalUnreadCount(),
4827+
);
4828+
}());
47634829
}
47644830

4765-
void _handleIncomingChannelMessage(Uint8List frame) {
4831+
void _handleIncomingChannelMessage(Uint8List frame) async {
4832+
// If we're syncing the queued messages, advance the queue immediately
4833+
// before any potentially long async work (like translation/notifications).
4834+
if (_isSyncingQueuedMessages) {
4835+
_handleQueuedMessageReceived();
4836+
}
47664837
final parsed = ChannelMessage.fromFrame(frame);
47674838
if (parsed != null && parsed.channelIndex != null) {
47684839
if (_shouldDropSelfChannelMessage(parsed.senderName, parsed.pathBytes)) {
@@ -4781,23 +4852,25 @@ class MeshCoreConnector extends ChangeNotifier {
47814852
pathBytes: message.pathBytes,
47824853
);
47834854
final isNew = _addChannelMessage(message.channelIndex!, message);
4784-
if (isNew && !message.isOutgoing) {
4785-
unawaited(
4786-
_translateIncomingChannelMessage(message.channelIndex!, message),
4787-
);
4788-
}
47894855
_maybeIncrementChannelUnread(message, isNew: isNew);
47904856
notifyListeners();
4791-
if (isNew) {
4792-
_maybeNotifyChannelMessage(message);
4857+
if (isNew && !message.isOutgoing) {
4858+
final msg = message; // capture for closure
4859+
unawaited(() async {
4860+
final translationResult = await translateChannelMessage(
4861+
msg.channelIndex!,
4862+
msg,
4863+
);
4864+
_maybeNotifyChannelMessage(msg, translationResult: translationResult);
4865+
}());
47934866
}
47944867
_handleQueuedMessageReceived();
47954868
} else if (_isSyncingQueuedMessages) {
47964869
_handleQueuedMessageReceived();
47974870
}
47984871
}
47994872

4800-
void _handleLogRxData(Uint8List frame) {
4873+
void _handleLogRxData(Uint8List frame) async {
48014874
if (frame.length < 4) return;
48024875
try {
48034876
final reader = BufferReader(frame);
@@ -4865,16 +4938,24 @@ class MeshCoreConnector extends ChangeNotifier {
48654938
pathBytes: message.pathBytes,
48664939
);
48674940
final isNew = _addChannelMessage(channel.index, message);
4868-
if (isNew && !message.isOutgoing) {
4869-
unawaited(_translateIncomingChannelMessage(channel.index, message));
4870-
}
48714941
_maybeIncrementChannelUnread(message, isNew: isNew);
48724942
notifyListeners();
48734943
if (isNew) {
4874-
final label = channel.name.isEmpty
4875-
? 'Channel ${channel.index}'
4876-
: channel.name;
4877-
_maybeNotifyChannelMessage(message, channelName: label);
4944+
// Run translation + notification asynchronously to avoid blocking
4945+
unawaited(() async {
4946+
final translationResult = await translateChannelMessage(
4947+
channel.index,
4948+
message,
4949+
);
4950+
final label = channel.name.isEmpty
4951+
? 'Channel ${channel.index}'
4952+
: channel.name;
4953+
_maybeNotifyChannelMessage(
4954+
message,
4955+
channelName: label,
4956+
translationResult: translationResult,
4957+
);
4958+
}());
48784959
}
48794960
return;
48804961
} catch (e) {

lib/l10n/app_bg.arb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2099,6 +2099,9 @@
20992099
"translation_composerTitle": "Преведете преди да изпратите",
21002100
"translation_enableSubtitle": "Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.",
21012101
"translation_composerSubtitle": "Контролира началния статус на иконата за превод, създадена от композитора.",
2102+
"translation_autoIncomingTitle": "Автоматичен превод на съобщения",
2103+
"translation_autoIncomingSubtitle": "Превежда автоматично съобщенията за известия, както и за чатове или канали.",
2104+
"translation_translateMessage": "Преведи съобщението",
21022105
"translation_targetLanguage": "Целеви език",
21032106
"translation_useAppLanguage": "Използвайте езика на приложението",
21042107
"translation_downloadedModelLabel": "Изтегнат модел",

lib/l10n/app_de.arb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2127,6 +2127,9 @@
21272127
"translation_enableSubtitle": "Nachrichten empfangen und übersetzen sowie die Möglichkeit bieten, Nachrichten vor dem Versenden zu übersetzen.",
21282128
"translation_enableTitle": "Aktivieren Sie die Übersetzung",
21292129
"translation_composerSubtitle": "Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.",
2130+
"translation_autoIncomingTitle": "Nachrichten automatisch übersetzen",
2131+
"translation_autoIncomingSubtitle": "Übersetzt Nachrichten für Benachrichtigungen sowie für Chats oder Kanäle automatisch.",
2132+
"translation_translateMessage": "Nachricht übersetzen",
21302133
"translation_targetLanguage": "Zielsprache",
21312134
"translation_useAppLanguage": "Verwenden Sie die App-Sprache",
21322135
"translation_downloadedModelLabel": "Heruntergeladenes Modell",

lib/l10n/app_en.arb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2288,6 +2288,9 @@
22882288
"translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.",
22892289
"translation_composerTitle": "Translate before sending",
22902290
"translation_composerSubtitle": "Controls the default state of the composer translation icon.",
2291+
"translation_autoIncomingTitle": "Auto-translate incoming messages",
2292+
"translation_autoIncomingSubtitle": "Translates Messages for notification and for chat or channel automatically.",
2293+
"translation_translateMessage": "Translate message",
22912294
"translation_targetLanguage": "Target language",
22922295
"translation_useAppLanguage": "Use app language",
22932296
"translation_downloadedModelLabel": "Downloaded model",

lib/l10n/app_es.arb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2128,6 +2128,9 @@
21282128
"translation_enableTitle": "Habilitar la traducción",
21292129
"translation_composerTitle": "Traducir antes de enviar",
21302130
"translation_composerSubtitle": "Controla el estado predeterminado del icono de traducción del compositor.",
2131+
"translation_autoIncomingTitle": "Traducir mensajes automáticamente",
2132+
"translation_autoIncomingSubtitle": "Traduce mensajes para notificaciones y para chats o canales automáticamente.",
2133+
"translation_translateMessage": "Traducir mensaje",
21312134
"translation_targetLanguage": "Idioma de destino",
21322135
"translation_useAppLanguage": "Utilizar el idioma de la aplicación",
21332136
"translation_downloadedModelLabel": "Modelo descargado",

lib/l10n/app_fr.arb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2099,6 +2099,9 @@
20992099
"translation_title": "Traduction",
21002100
"translation_enableSubtitle": "Traduire les messages entrants et permettre la traduction avant l'envoi.",
21012101
"translation_composerSubtitle": "Contrôle l'état par défaut de l'icône de traduction du composant.",
2102+
"translation_autoIncomingTitle": "Traduire automatiquement les messages",
2103+
"translation_autoIncomingSubtitle": "Traduit automatiquement les messages pour les notifications et pour les discussions ou les canaux.",
2104+
"translation_translateMessage": "Traduire le message",
21022105
"translation_targetLanguage": "Langue cible",
21032106
"translation_useAppLanguage": "Utiliser la langue de l'application",
21042107
"translation_downloadedModelLabel": "Modèle téléchargé",

lib/l10n/app_hu.arb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2137,6 +2137,9 @@
21372137
"translation_enableSubtitle": "Fordítsa az érkező üzeneteket, és lehetővé tegye a küldés előtti fordítást.",
21382138
"translation_composerTitle": "Fordítsa el, mielőtt elküldi",
21392139
"translation_composerSubtitle": "Ellenőrzi a zeneszerző fordítási ikon alapértékét.",
2140+
"translation_autoIncomingTitle": "Üzenetek automatikus fordítása",
2141+
"translation_autoIncomingSubtitle": "Automatikusan lefordítja az üzeneteket az értesítésekhez, valamint a csevegésekhez vagy csatornákhoz.",
2142+
"translation_translateMessage": "Üzenet fordítása",
21402143
"translation_targetLanguage": "Célnyelv",
21412144
"translation_useAppLanguage": "Használja az alkalmazás nyelvének beállítását.",
21422145
"translation_downloadedModelLabel": "Letöltött modell",

lib/l10n/app_it.arb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2100,6 +2100,9 @@
21002100
"translation_enableTitle": "Abilitare la traduzione",
21012101
"translation_title": "Traduzione",
21022102
"translation_composerSubtitle": "Controlla lo stato predefinito dell'icona di traduzione del compositore.",
2103+
"translation_autoIncomingTitle": "Traduci automaticamente i messaggi",
2104+
"translation_autoIncomingSubtitle": "Traduce automaticamente i messaggi per le notifiche e per le chat o i canali.",
2105+
"translation_translateMessage": "Traduci messaggio",
21032106
"translation_targetLanguage": "Lingua di destinazione",
21042107
"translation_useAppLanguage": "Utilizza la lingua dell'app",
21052108
"translation_downloadedModelLabel": "Modello scaricato",

0 commit comments

Comments
 (0)