From bd6d181b2fb1f50b9d967217edab21e5af49f63d Mon Sep 17 00:00:00 2001 From: Strycher Date: Fri, 19 Jun 2026 04:07:26 -0400 Subject: [PATCH] feat(#37): compact message tile - decouple time, cell_tower heard, inline @mentions - Clock setting (System / 12h / 24h) in App Settings; chat times follow it + locale. - Channel tile: always-on metadata row - time + hops on inbound, time + cell_tower heard count + status on outbound - decoupled from the message-tracing toggle. The via-path stays behind tracing. - Drop the auto reply-to block and the most-recent-sender guess; @mentions now render inline in the message text. - Direct chat: same clock-aware time formatting. Closes #37 Co-Authored-By: Claude Opus 4.8 --- lib/connector/meshcore_connector.dart | 55 +--- lib/l10n/app_en.arb | 12 + lib/l10n/app_localizations.dart | 30 ++ lib/l10n/app_localizations_bg.dart | 23 ++ lib/l10n/app_localizations_de.dart | 23 ++ lib/l10n/app_localizations_en.dart | 23 ++ lib/l10n/app_localizations_es.dart | 23 ++ lib/l10n/app_localizations_fr.dart | 23 ++ lib/l10n/app_localizations_hu.dart | 23 ++ lib/l10n/app_localizations_it.dart | 23 ++ lib/l10n/app_localizations_ja.dart | 23 ++ lib/l10n/app_localizations_ko.dart | 23 ++ lib/l10n/app_localizations_nl.dart | 23 ++ lib/l10n/app_localizations_pl.dart | 23 ++ lib/l10n/app_localizations_pt.dart | 23 ++ lib/l10n/app_localizations_ru.dart | 23 ++ lib/l10n/app_localizations_sk.dart | 23 ++ lib/l10n/app_localizations_sl.dart | 23 ++ lib/l10n/app_localizations_sv.dart | 23 ++ lib/l10n/app_localizations_uk.dart | 23 ++ lib/l10n/app_localizations_zh.dart | 23 ++ lib/models/app_settings.dart | 32 +++ lib/screens/channel_chat_screen.dart | 303 +++++--------------- lib/screens/chat_screen.dart | 19 +- lib/screens/settings/app_settings_view.dart | 65 +++++ lib/services/app_settings_service.dart | 4 + untranslated.json | 85 ++++++ 27 files changed, 739 insertions(+), 280 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 9c25be2..eeaf785 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -5779,44 +5779,10 @@ class MeshCoreConnector extends ChangeNotifier { return false; // Don't add reaction as a visible message } - // Parse reply info from message text - final replyInfo = ChannelMessage.parseReplyMention(message.text); - ChannelMessage processedMessage = message; - - if (replyInfo != null) { - // Find original message by sender name (most recent match) - final originalMessage = _findMessageBySender( - messages, - replyInfo.mentionedNode, - ); - - if (originalMessage != null) { - // Create new message with reply metadata - processedMessage = ChannelMessage( - senderKey: message.senderKey, - senderName: message.senderName, - text: replyInfo.actualMessage, - originalText: message.originalText, - translatedText: message.translatedText, - translatedLanguageCode: message.translatedLanguageCode, - translationStatus: message.translationStatus, - translationModelId: message.translationModelId, - timestamp: message.timestamp, - isOutgoing: message.isOutgoing, - status: message.status, - repeats: message.repeats, - repeatCount: message.repeatCount, - pathLength: message.pathLength, - pathBytes: message.pathBytes, - pathVariants: message.pathVariants, - channelIndex: message.channelIndex, - messageId: message.messageId, - replyToMessageId: originalMessage.messageId, - replyToSenderName: originalMessage.senderName, - replyToText: originalMessage.text, - ); - } - } + // @mentions render inline in the message text; we no longer synthesize an + // auto reply-block (it guessed the target by most-recent sender, which + // mis-attributed and ate screen space). See #37. + final ChannelMessage processedMessage = message; final existingIndex = _findChannelRepeatIndex(messages, processedMessage); var isNew = true; @@ -5863,19 +5829,6 @@ class MeshCoreConnector extends ChangeNotifier { return isNew; } - ChannelMessage? _findMessageBySender( - List messages, - String mentionedNode, - ) { - // Search backwards for most recent message from this sender - for (int i = messages.length - 1; i >= 0; i--) { - if (messages[i].senderName == mentionedNode && !messages[i].isOutgoing) { - return messages[i]; - } - } - return null; - } - void _processReaction( List messages, ReactionInfo reactionInfo, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 341ffef..4924aa4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -255,6 +255,10 @@ "appSettings_themeSystem": "System default", "appSettings_themeLight": "Light", "appSettings_themeDark": "Dark", + "appSettings_clock": "Clock", + "appSettings_clockSystem": "System default", + "appSettings_clock12h": "12-hour", + "appSettings_clock24h": "24-hour", "appSettings_language": "Language", "appSettings_languageSystem": "System default", "appSettings_languageEn": "English", @@ -802,6 +806,14 @@ } } }, + "channel_heardTooltip": "Heard {count, plural, =1{1 repeat} other{{count} repeats}}", + "@channel_heardTooltip": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "chat_successes": "successes", "chat_score": "Score", "chat_removePath": "Remove path", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8dc8f4b..ccdd201 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1246,6 +1246,30 @@ abstract class AppLocalizations { /// **'Dark'** String get appSettings_themeDark; + /// No description provided for @appSettings_clock. + /// + /// In en, this message translates to: + /// **'Clock'** + String get appSettings_clock; + + /// No description provided for @appSettings_clockSystem. + /// + /// In en, this message translates to: + /// **'System default'** + String get appSettings_clockSystem; + + /// No description provided for @appSettings_clock12h. + /// + /// In en, this message translates to: + /// **'12-hour'** + String get appSettings_clock12h; + + /// No description provided for @appSettings_clock24h. + /// + /// In en, this message translates to: + /// **'24-hour'** + String get appSettings_clock24h; + /// No description provided for @appSettings_language. /// /// In en, this message translates to: @@ -2782,6 +2806,12 @@ abstract class AppLocalizations { /// **'{count} {count, plural, =1{hop} other{hops}}'** String chat_hopsCount(int count); + /// No description provided for @channel_heardTooltip. + /// + /// In en, this message translates to: + /// **'Heard {count, plural, =1{1 repeat} other{{count} repeats}}'** + String channel_heardTooltip(int count); + /// No description provided for @chat_successes. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 0109048..88cbfd6 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -628,6 +628,18 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_themeDark => 'Тъмно'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Език'; @@ -1529,6 +1541,17 @@ class AppLocalizationsBg extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'Успехи'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 6e9a761..7d30fc3 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -623,6 +623,18 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_themeDark => 'Dunkel'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Sprache'; @@ -1528,6 +1540,17 @@ class AppLocalizationsDe extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'Erfolgreich'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ddf0eae..347190a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -613,6 +613,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appSettings_themeDark => 'Dark'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Language'; @@ -1498,6 +1510,17 @@ class AppLocalizationsEn extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'successes'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e30c11a..fcd09c2 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -624,6 +624,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_themeDark => 'Oscuro'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Idioma'; @@ -1525,6 +1537,17 @@ class AppLocalizationsEs extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'Éxitos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 547027b..95e4bbc 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -628,6 +628,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_themeDark => 'Sombre'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Langue'; @@ -1532,6 +1544,17 @@ class AppLocalizationsFr extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'Succès'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 2965208..ad84875 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -628,6 +628,18 @@ class AppLocalizationsHu extends AppLocalizations { @override String get appSettings_themeDark => 'Sötét'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Nyelv'; @@ -1536,6 +1548,17 @@ class AppLocalizationsHu extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'sikerek'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 9d62aa5..73eb2d0 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -626,6 +626,18 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_themeDark => 'Scuro'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Lingua'; @@ -1527,6 +1539,17 @@ class AppLocalizationsIt extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'successi'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 4828824..9e7d910 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -599,6 +599,18 @@ class AppLocalizationsJa extends AppLocalizations { @override String get appSettings_themeDark => '暗い'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => '言語'; @@ -1461,6 +1473,17 @@ class AppLocalizationsJa extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => '成功事例'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index dba63f6..08ee2a5 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -599,6 +599,18 @@ class AppLocalizationsKo extends AppLocalizations { @override String get appSettings_themeDark => '어둡다'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => '언어'; @@ -1457,6 +1469,17 @@ class AppLocalizationsKo extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => '성공 사례'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 72e540b..40e2202 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -620,6 +620,18 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appSettings_themeDark => 'Donker'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Taal'; @@ -1514,6 +1526,17 @@ class AppLocalizationsNl extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'Succesvol'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 3c4c6cb..5becab4 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -629,6 +629,18 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_themeDark => 'Ciemny'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Język'; @@ -1540,6 +1552,17 @@ class AppLocalizationsPl extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'Sukcesy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 99d02cf..e65dc4d 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -626,6 +626,18 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_themeDark => 'Escuro'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Idioma'; @@ -1524,6 +1536,17 @@ class AppLocalizationsPt extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'Sucessos'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 33e38b6..319eb02 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -626,6 +626,18 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appSettings_themeDark => 'Тёмная'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Язык'; @@ -1528,6 +1540,17 @@ class AppLocalizationsRu extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'успешно'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index e1cc5e0..43020e6 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -620,6 +620,18 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_themeDark => 'Tmavé'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Jazyk'; @@ -1516,6 +1528,17 @@ class AppLocalizationsSk extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'Úspechy'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ea3686b..75e0572 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -617,6 +617,18 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_themeDark => 'Temno'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Jezik'; @@ -1512,6 +1524,17 @@ class AppLocalizationsSl extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'Uspešni'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index ffd28a4..eba3800 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -615,6 +615,18 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_themeDark => 'Mörk'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Språk'; @@ -1507,6 +1519,17 @@ class AppLocalizationsSv extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'framgångar'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 821e4a6..5c7d26c 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -623,6 +623,18 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_themeDark => 'Темна'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => 'Мова'; @@ -1523,6 +1535,17 @@ class AppLocalizationsUk extends AppLocalizations { return '$count $_temp0'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => 'Успішно'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index d01ae3c..8e747d1 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -590,6 +590,18 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_themeDark => '深色'; + @override + String get appSettings_clock => 'Clock'; + + @override + String get appSettings_clockSystem => 'System default'; + + @override + String get appSettings_clock12h => '12-hour'; + + @override + String get appSettings_clock24h => '24-hour'; + @override String get appSettings_language => '语言'; @@ -1436,6 +1448,17 @@ class AppLocalizationsZh extends AppLocalizations { return '$count 跳'; } + @override + String channel_heardTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count repeats', + one: '1 repeat', + ); + return 'Heard $_temp0'; + } + @override String get chat_successes => '成功'; diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 6995b6e..6dc8581 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -13,6 +13,21 @@ extension UnitSystemValue on UnitSystem { } } +enum ClockFormat { system, twelveHour, twentyFourHour } + +extension ClockFormatValue on ClockFormat { + String get value { + switch (this) { + case ClockFormat.twelveHour: + return '12h'; + case ClockFormat.twentyFourHour: + return '24h'; + case ClockFormat.system: + return 'system'; + } + } +} + const Map defaultCyr2LatCharMap = { 'А': 'A', 'В': 'B', @@ -88,6 +103,7 @@ class AppSettings { final bool mapShowMarkers; final bool mapShowGuessedLocations; final bool enableMessageTracing; + final ClockFormat clockFormat; final Map? mapCacheBounds; final int mapCacheMinZoom; final int mapCacheMaxZoom; @@ -142,6 +158,7 @@ class AppSettings { this.mapShowMarkers = true, this.mapShowGuessedLocations = true, this.enableMessageTracing = false, + this.clockFormat = ClockFormat.system, this.mapCacheBounds, this.mapCacheMinZoom = 10, this.mapCacheMaxZoom = 15, @@ -203,6 +220,7 @@ class AppSettings { 'map_show_markers': mapShowMarkers, 'map_show_guessed_locations': mapShowGuessedLocations, 'enable_message_tracing': enableMessageTracing, + 'clock_format': clockFormat.value, 'map_cache_bounds': mapCacheBounds, 'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_max_zoom': mapCacheMaxZoom, @@ -251,6 +269,17 @@ class AppSettings { return UnitSystem.metric; } + ClockFormat parseClockFormat(dynamic value) { + switch (value) { + case '12h': + return ClockFormat.twelveHour; + case '24h': + return ClockFormat.twentyFourHour; + default: + return ClockFormat.system; + } + } + return AppSettings( clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false, mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true, @@ -265,6 +294,7 @@ class AppSettings { mapShowGuessedLocations: json['map_show_guessed_locations'] as bool? ?? true, enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, + clockFormat: parseClockFormat(json['clock_format']), mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( (key, value) => MapEntry(key.toString(), (value as num).toDouble()), ), @@ -376,6 +406,7 @@ class AppSettings { bool? mapShowMarkers, bool? mapShowGuessedLocations, bool? enableMessageTracing, + ClockFormat? clockFormat, Object? mapCacheBounds = _unset, int? mapCacheMinZoom, int? mapCacheMaxZoom, @@ -423,6 +454,7 @@ class AppSettings { mapShowGuessedLocations: mapShowGuessedLocations ?? this.mapShowGuessedLocations, enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing, + clockFormat: clockFormat ?? this.clockFormat, mapCacheBounds: mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map?, diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 256546b..84648ea 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -22,6 +22,7 @@ import '../l10n/l10n.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; import '../models/translation_support.dart'; +import '../models/app_settings.dart'; import '../services/app_settings_service.dart'; import '../services/chat_text_scale_service.dart'; import '../services/translation_service.dart'; @@ -34,7 +35,6 @@ import '../widgets/gif_message.dart'; import '../widgets/jump_to_bottom_button.dart'; import '../widgets/gif_picker.dart'; import '../widgets/message_translation_button.dart'; -import '../widgets/message_status_icon.dart'; import '../widgets/radio_stats_entry.dart'; import '../widgets/sync_progress_overlay.dart'; import '../widgets/translated_message_content.dart'; @@ -574,10 +574,6 @@ class _ChannelChatScreenState extends State { ), if (gifId == null) const SizedBox(height: 4), ], - if (message.replyToMessageId != null) ...[ - _buildReplyPreview(message, textScale), - const SizedBox(height: 8), - ], if (poi != null) _buildPoiMessage( context, @@ -585,20 +581,6 @@ class _ChannelChatScreenState extends State { isOutgoing, textScale, message.senderName, - trailing: (!enableTracing && isOutgoing) - ? Padding( - padding: const EdgeInsets.only(bottom: 2), - child: MessageStatusIcon( - isAcked: - message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty, - isFailed: - message.status == - ChannelMessageStatus.failed, - ), - ) - : null, ) else if (gifId != null) Stack( @@ -618,36 +600,6 @@ class _ChannelChatScreenState extends State { .withValues(alpha: 0.6), ), ), - if (!enableTracing && isOutgoing) - Positioned( - top: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(3), - decoration: BoxDecoration( - color: isOutgoing - ? Theme.of( - context, - ).colorScheme.primaryContainer - : Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(10), - topRight: Radius.circular(8), - ), - ), - child: MessageStatusIcon( - isAcked: - message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty, - isFailed: - message.status == - ChannelMessageStatus.failed, - ), - ), - ), ], ) else @@ -670,106 +622,27 @@ class _ChannelChatScreenState extends State { ), ), ), - if (!enableTracing && isOutgoing) ...[ - const SizedBox(width: 4), - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: MessageStatusIcon( - isAcked: - message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty, - isFailed: - message.status == - ChannelMessageStatus.failed, - ), - ), - ], ], ), - if (message.hopCount != null && - message.hopCount! > 0) ...[ - const SizedBox(height: 4), - _buildHopBadge(context, message), - ], - if (enableTracing) ...[ - if (displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.channels_via( - _formatPathPrefixes( - displayPath, - message.pathHashSize, - ), - ), - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ), - ], - const SizedBox(height: 4), + const SizedBox(height: 4), + _buildMetaRow(context, message, isOutgoing), + if (enableTracing && displayPath.isNotEmpty) ...[ + const SizedBox(height: 2), Padding( padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) + ? const EdgeInsets.symmetric(horizontal: 8) : EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _formatTime(context, message.timestamp), - style: TextStyle( - fontSize: 11, - color: - Theme.of(context).brightness == - Brightness.dark - ? Colors.grey[400] - : Colors.grey[600], - ), + child: Text( + context.l10n.channels_via( + _formatPathPrefixes( + displayPath, + message.pathHashSize, ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon( - Icons.repeat, - size: 12, - color: Colors.grey[600], - ), - const SizedBox(width: 2), - Text( - '${message.repeatCount}', - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - Icon( - message.status == ChannelMessageStatus.sent - ? Icons.check - : message.status == - ChannelMessageStatus.pending - ? Icons.schedule - : Icons.error_outline, - size: 14, - color: - message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], - ], + ), + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), ), ), ], @@ -847,83 +720,6 @@ class _ChannelChatScreenState extends State { ); } - Widget _buildReplyPreview(ChannelMessage message, double textScale) { - final connector = context.read(); - final isOwnNode = message.replyToSenderName == connector.selfName; - final replyText = message.replyToText ?? ''; - final colorScheme = Theme.of(context).colorScheme; - final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7); - - final gifId = GifHelper.parseGif(replyText); - final poi = parseMarkerText(replyText); - - Widget contentPreview; - if (gifId != null) { - contentPreview = ClipRRect( - borderRadius: BorderRadius.circular(4), - child: GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: colorScheme.surfaceContainerHighest, - fallbackTextColor: previewTextColor, - maxSize: 80, - ), - ); - } else if (poi != null) { - contentPreview = Row( - children: [ - Icon(Icons.location_on_outlined, size: 14, color: previewTextColor), - const SizedBox(width: 4), - Text( - context.l10n.chat_location, - style: TextStyle(fontSize: 12 * textScale, color: previewTextColor), - ), - ], - ); - } else { - contentPreview = Text( - replyText, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12 * textScale, - color: previewTextColor, - fontStyle: FontStyle.italic, - ), - ); - } - - return GestureDetector( - onTap: () => _scrollToMessage(message.replyToMessageId!), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(8), - border: Border( - left: BorderSide(color: colorScheme.primary, width: 3), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.chat_replyTo(message.replyToSenderName ?? ''), - style: TextStyle( - fontSize: 11 * textScale, - fontWeight: FontWeight.bold, - color: isOwnNode - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 2), - contentPreview, - ], - ), - ), - ); - } - Widget _buildReactionsDisplay(ChannelMessage message) { return Wrap( spacing: 6, @@ -1427,19 +1223,78 @@ class _ChannelChatScreenState extends State { void _ensureDateFormats(BuildContext context) { final locale = Localizations.localeOf(context).toString(); - if (locale != _cachedFormatLocale) { - _cachedFormatLocale = locale; - _hmFormat = DateFormat.Hm(locale); + final clock = context.read().settings.clockFormat; + final systemUses24 = MediaQuery.of(context).alwaysUse24HourFormat; + final key = '$locale|${clock.index}|$systemUses24'; + if (key != _cachedFormatLocale) { + _cachedFormatLocale = key; + _hmFormat = switch (clock) { + ClockFormat.twentyFourHour => DateFormat.Hm(locale), + ClockFormat.twelveHour => DateFormat('h:mm a', locale), + ClockFormat.system => + systemUses24 ? DateFormat.Hm(locale) : DateFormat.jm(locale), + }; _mdFormat = DateFormat.Md(locale); } } String _formatTime(BuildContext context, DateTime time) { _ensureDateFormats(context); - // Always show absolute HH:mm; the calendar date is shown via day dividers. + // Time per the user's clock setting; the date is shown via day dividers. return _hmFormat.format(time); } + Widget _buildMetaRow( + BuildContext context, + ChannelMessage message, + bool isOutgoing, + ) { + final metaColor = Theme.of(context).brightness == Brightness.dark + ? Colors.grey[400] + : Colors.grey[600]; + final metaStyle = TextStyle(fontSize: 11, color: metaColor); + final hops = message.hopCount ?? 0; + final dot = Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Text('·', style: metaStyle), + ); + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_formatTime(context, message.timestamp), style: metaStyle), + if (!isOutgoing && hops > 0) ...[dot, _buildHopBadge(context, message)], + if (isOutgoing && message.repeatCount > 0) ...[ + dot, + Tooltip( + message: context.l10n.channel_heardTooltip(message.repeatCount), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.cell_tower, size: 12, color: metaColor), + const SizedBox(width: 4), + Text('${message.repeatCount}', style: metaStyle), + ], + ), + ), + ], + if (isOutgoing) ...[ + const SizedBox(width: 6), + Icon( + message.status == ChannelMessageStatus.sent + ? Icons.check + : message.status == ChannelMessageStatus.pending + ? Icons.schedule + : Icons.error_outline, + size: 14, + color: message.status == ChannelMessageStatus.failed + ? Colors.red + : metaColor, + ), + ], + ], + ); + } + bool _isSameDay(DateTime a, DateTime b) => a.year == b.year && a.month == b.month && a.day == b.day; diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index b763aa3..3ef2364 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -7,6 +7,7 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; import '../utils/platform_info.dart'; @@ -22,6 +23,7 @@ import '../models/channel_message.dart'; import '../models/contact.dart'; import '../l10n/contact_localization.dart'; import '../models/message.dart'; +import '../models/app_settings.dart'; import '../models/path_history.dart'; import '../models/translation_support.dart'; import '../services/app_settings_service.dart'; @@ -1993,7 +1995,7 @@ class _MessageBubble extends StatelessWidget { crossAxisAlignment: WrapCrossAlignment.center, children: [ Text( - _formatTime(message.timestamp), + _formatTime(context, message.timestamp), style: TextStyle( fontSize: 10, color: metaColor, @@ -2254,9 +2256,16 @@ class _MessageBubble extends StatelessWidget { return Icon(icon, size: 12, color: color); } - String _formatTime(DateTime time) { - final hour = time.hour.toString().padLeft(2, '0'); - final minute = time.minute.toString().padLeft(2, '0'); - return '$hour:$minute'; + String _formatTime(BuildContext context, DateTime time) { + final locale = Localizations.localeOf(context).toString(); + final clock = context.read().settings.clockFormat; + final systemUses24 = MediaQuery.of(context).alwaysUse24HourFormat; + final fmt = switch (clock) { + ClockFormat.twentyFourHour => DateFormat.Hm(locale), + ClockFormat.twelveHour => DateFormat('h:mm a', locale), + ClockFormat.system => + systemUses24 ? DateFormat.Hm(locale) : DateFormat.jm(locale), + }; + return fmt.format(time); } } diff --git a/lib/screens/settings/app_settings_view.dart b/lib/screens/settings/app_settings_view.dart index c4e2421..c238857 100644 --- a/lib/screens/settings/app_settings_view.dart +++ b/lib/screens/settings/app_settings_view.dart @@ -75,6 +75,16 @@ class AppSettingsView extends StatelessWidget { onTap: () => _showThemeModeDialog(context, settingsService), ), const Divider(height: 1), + ListTile( + leading: const Icon(Icons.schedule_outlined), + title: Text(context.l10n.appSettings_clock), + subtitle: Text( + _clockFormatLabel(context, settingsService.settings.clockFormat), + ), + trailing: const Icon(Icons.chevron_right), + onTap: () => _showClockFormatDialog(context, settingsService), + ), + const Divider(height: 1), ListTile( leading: const Icon(Icons.language_outlined), title: Text(context.l10n.appSettings_language), @@ -570,6 +580,61 @@ class AppSettingsView extends StatelessWidget { } } + void _showClockFormatDialog( + BuildContext context, + AppSettingsService settingsService, + ) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.l10n.appSettings_clock), + content: RadioGroup( + groupValue: settingsService.settings.clockFormat, + onChanged: (value) { + if (value != null) { + settingsService.setClockFormat(value); + Navigator.pop(context); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RadioListTile( + title: Text(context.l10n.appSettings_clockSystem), + value: ClockFormat.system, + ), + RadioListTile( + title: Text(context.l10n.appSettings_clock12h), + value: ClockFormat.twelveHour, + ), + RadioListTile( + title: Text(context.l10n.appSettings_clock24h), + value: ClockFormat.twentyFourHour, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.common_close), + ), + ], + ), + ); + } + + String _clockFormatLabel(BuildContext context, ClockFormat value) { + switch (value) { + case ClockFormat.twelveHour: + return context.l10n.appSettings_clock12h; + case ClockFormat.twentyFourHour: + return context.l10n.appSettings_clock24h; + case ClockFormat.system: + return context.l10n.appSettings_clockSystem; + } + } + String _languageLabel(BuildContext context, String? languageCode) { switch (languageCode) { case 'en': diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index 88ff0f4..7fc09ca 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -100,6 +100,10 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(enableMessageTracing: value)); } + Future setClockFormat(ClockFormat value) async { + await updateSettings(_settings.copyWith(clockFormat: value)); + } + Future setMapCacheBounds(Map? value) async { await updateSettings(_settings.copyWith(mapCacheBounds: value)); } diff --git a/untranslated.json b/untranslated.json index 8da0aa2..c894cb7 100644 --- a/untranslated.json +++ b/untranslated.json @@ -2,102 +2,187 @@ "bg": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "de": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "es": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "fr": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "hu": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "it": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "ja": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "ko": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "nl": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "pl": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "pt": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "ru": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "sk": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "sl": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "sv": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "uk": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ], "zh": [ "settings_messageSettings", "settings_messageSettingsSubtitle", + "appSettings_clock", + "appSettings_clockSystem", + "appSettings_clock12h", + "appSettings_clock24h", + "channel_heardTooltip", "snrIndicator_noNeighbors" ] }