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 <noreply@anthropic.com>
pull/48/head
Strycher 1 month ago
parent 1aaffb380d
commit bd6d181b2f

@ -5779,44 +5779,10 @@ class MeshCoreConnector extends ChangeNotifier {
return false; // Don't add reaction as a visible message return false; // Don't add reaction as a visible message
} }
// Parse reply info from message text // @mentions render inline in the message text; we no longer synthesize an
final replyInfo = ChannelMessage.parseReplyMention(message.text); // auto reply-block (it guessed the target by most-recent sender, which
ChannelMessage processedMessage = message; // mis-attributed and ate screen space). See #37.
final 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,
);
}
}
final existingIndex = _findChannelRepeatIndex(messages, processedMessage); final existingIndex = _findChannelRepeatIndex(messages, processedMessage);
var isNew = true; var isNew = true;
@ -5863,19 +5829,6 @@ class MeshCoreConnector extends ChangeNotifier {
return isNew; return isNew;
} }
ChannelMessage? _findMessageBySender(
List<ChannelMessage> 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( void _processReaction(
List<ChannelMessage> messages, List<ChannelMessage> messages,
ReactionInfo reactionInfo, ReactionInfo reactionInfo,

@ -255,6 +255,10 @@
"appSettings_themeSystem": "System default", "appSettings_themeSystem": "System default",
"appSettings_themeLight": "Light", "appSettings_themeLight": "Light",
"appSettings_themeDark": "Dark", "appSettings_themeDark": "Dark",
"appSettings_clock": "Clock",
"appSettings_clockSystem": "System default",
"appSettings_clock12h": "12-hour",
"appSettings_clock24h": "24-hour",
"appSettings_language": "Language", "appSettings_language": "Language",
"appSettings_languageSystem": "System default", "appSettings_languageSystem": "System default",
"appSettings_languageEn": "English", "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_successes": "successes",
"chat_score": "Score", "chat_score": "Score",
"chat_removePath": "Remove path", "chat_removePath": "Remove path",

@ -1246,6 +1246,30 @@ abstract class AppLocalizations {
/// **'Dark'** /// **'Dark'**
String get appSettings_themeDark; 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. /// No description provided for @appSettings_language.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2782,6 +2806,12 @@ abstract class AppLocalizations {
/// **'{count} {count, plural, =1{hop} other{hops}}'** /// **'{count} {count, plural, =1{hop} other{hops}}'**
String chat_hopsCount(int count); 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. /// No description provided for @chat_successes.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

@ -628,6 +628,18 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Тъмно'; 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 @override
String get appSettings_language => 'Език'; String get appSettings_language => 'Език';
@ -1529,6 +1541,17 @@ class AppLocalizationsBg extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'Успехи'; String get chat_successes => 'Успехи';

@ -623,6 +623,18 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Dunkel'; 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 @override
String get appSettings_language => 'Sprache'; String get appSettings_language => 'Sprache';
@ -1528,6 +1540,17 @@ class AppLocalizationsDe extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'Erfolgreich'; String get chat_successes => 'Erfolgreich';

@ -613,6 +613,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Dark'; 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 @override
String get appSettings_language => 'Language'; String get appSettings_language => 'Language';
@ -1498,6 +1510,17 @@ class AppLocalizationsEn extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'successes'; String get chat_successes => 'successes';

@ -624,6 +624,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Oscuro'; 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 @override
String get appSettings_language => 'Idioma'; String get appSettings_language => 'Idioma';
@ -1525,6 +1537,17 @@ class AppLocalizationsEs extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'Éxitos'; String get chat_successes => 'Éxitos';

@ -628,6 +628,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Sombre'; 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 @override
String get appSettings_language => 'Langue'; String get appSettings_language => 'Langue';
@ -1532,6 +1544,17 @@ class AppLocalizationsFr extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'Succès'; String get chat_successes => 'Succès';

@ -628,6 +628,18 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Sötét'; 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 @override
String get appSettings_language => 'Nyelv'; String get appSettings_language => 'Nyelv';
@ -1536,6 +1548,17 @@ class AppLocalizationsHu extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'sikerek'; String get chat_successes => 'sikerek';

@ -626,6 +626,18 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Scuro'; 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 @override
String get appSettings_language => 'Lingua'; String get appSettings_language => 'Lingua';
@ -1527,6 +1539,17 @@ class AppLocalizationsIt extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'successi'; String get chat_successes => 'successi';

@ -599,6 +599,18 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get appSettings_themeDark => '暗い'; 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 @override
String get appSettings_language => '言語'; String get appSettings_language => '言語';
@ -1461,6 +1473,17 @@ class AppLocalizationsJa extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => '成功事例'; String get chat_successes => '成功事例';

@ -599,6 +599,18 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get appSettings_themeDark => '어둡다'; 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 @override
String get appSettings_language => '언어'; String get appSettings_language => '언어';
@ -1457,6 +1469,17 @@ class AppLocalizationsKo extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => '성공 사례'; String get chat_successes => '성공 사례';

@ -620,6 +620,18 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Donker'; 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 @override
String get appSettings_language => 'Taal'; String get appSettings_language => 'Taal';
@ -1514,6 +1526,17 @@ class AppLocalizationsNl extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'Succesvol'; String get chat_successes => 'Succesvol';

@ -629,6 +629,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Ciemny'; 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 @override
String get appSettings_language => 'Język'; String get appSettings_language => 'Język';
@ -1540,6 +1552,17 @@ class AppLocalizationsPl extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'Sukcesy'; String get chat_successes => 'Sukcesy';

@ -626,6 +626,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Escuro'; 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 @override
String get appSettings_language => 'Idioma'; String get appSettings_language => 'Idioma';
@ -1524,6 +1536,17 @@ class AppLocalizationsPt extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'Sucessos'; String get chat_successes => 'Sucessos';

@ -626,6 +626,18 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Тёмная'; 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 @override
String get appSettings_language => 'Язык'; String get appSettings_language => 'Язык';
@ -1528,6 +1540,17 @@ class AppLocalizationsRu extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'успешно'; String get chat_successes => 'успешно';

@ -620,6 +620,18 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Tmavé'; 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 @override
String get appSettings_language => 'Jazyk'; String get appSettings_language => 'Jazyk';
@ -1516,6 +1528,17 @@ class AppLocalizationsSk extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'Úspechy'; String get chat_successes => 'Úspechy';

@ -617,6 +617,18 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Temno'; 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 @override
String get appSettings_language => 'Jezik'; String get appSettings_language => 'Jezik';
@ -1512,6 +1524,17 @@ class AppLocalizationsSl extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'Uspešni'; String get chat_successes => 'Uspešni';

@ -615,6 +615,18 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Mörk'; 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 @override
String get appSettings_language => 'Språk'; String get appSettings_language => 'Språk';
@ -1507,6 +1519,17 @@ class AppLocalizationsSv extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'framgångar'; String get chat_successes => 'framgångar';

@ -623,6 +623,18 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get appSettings_themeDark => 'Темна'; 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 @override
String get appSettings_language => 'Мова'; String get appSettings_language => 'Мова';
@ -1523,6 +1535,17 @@ class AppLocalizationsUk extends AppLocalizations {
return '$count $_temp0'; 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 @override
String get chat_successes => 'Успішно'; String get chat_successes => 'Успішно';

@ -590,6 +590,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get appSettings_themeDark => '深色'; 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 @override
String get appSettings_language => '语言'; String get appSettings_language => '语言';
@ -1436,6 +1448,17 @@ class AppLocalizationsZh extends AppLocalizations {
return '$count'; 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 @override
String get chat_successes => '成功'; String get chat_successes => '成功';

@ -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<String, String> defaultCyr2LatCharMap = { const Map<String, String> defaultCyr2LatCharMap = {
'А': 'A', 'А': 'A',
'В': 'B', 'В': 'B',
@ -88,6 +103,7 @@ class AppSettings {
final bool mapShowMarkers; final bool mapShowMarkers;
final bool mapShowGuessedLocations; final bool mapShowGuessedLocations;
final bool enableMessageTracing; final bool enableMessageTracing;
final ClockFormat clockFormat;
final Map<String, double>? mapCacheBounds; final Map<String, double>? mapCacheBounds;
final int mapCacheMinZoom; final int mapCacheMinZoom;
final int mapCacheMaxZoom; final int mapCacheMaxZoom;
@ -142,6 +158,7 @@ class AppSettings {
this.mapShowMarkers = true, this.mapShowMarkers = true,
this.mapShowGuessedLocations = true, this.mapShowGuessedLocations = true,
this.enableMessageTracing = false, this.enableMessageTracing = false,
this.clockFormat = ClockFormat.system,
this.mapCacheBounds, this.mapCacheBounds,
this.mapCacheMinZoom = 10, this.mapCacheMinZoom = 10,
this.mapCacheMaxZoom = 15, this.mapCacheMaxZoom = 15,
@ -203,6 +220,7 @@ class AppSettings {
'map_show_markers': mapShowMarkers, 'map_show_markers': mapShowMarkers,
'map_show_guessed_locations': mapShowGuessedLocations, 'map_show_guessed_locations': mapShowGuessedLocations,
'enable_message_tracing': enableMessageTracing, 'enable_message_tracing': enableMessageTracing,
'clock_format': clockFormat.value,
'map_cache_bounds': mapCacheBounds, 'map_cache_bounds': mapCacheBounds,
'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_min_zoom': mapCacheMinZoom,
'map_cache_max_zoom': mapCacheMaxZoom, 'map_cache_max_zoom': mapCacheMaxZoom,
@ -251,6 +269,17 @@ class AppSettings {
return UnitSystem.metric; 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( return AppSettings(
clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false, clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false,
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true, mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
@ -265,6 +294,7 @@ class AppSettings {
mapShowGuessedLocations: mapShowGuessedLocations:
json['map_show_guessed_locations'] as bool? ?? true, json['map_show_guessed_locations'] as bool? ?? true,
enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, enableMessageTracing: json['enable_message_tracing'] as bool? ?? false,
clockFormat: parseClockFormat(json['clock_format']),
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
(key, value) => MapEntry(key.toString(), (value as num).toDouble()), (key, value) => MapEntry(key.toString(), (value as num).toDouble()),
), ),
@ -376,6 +406,7 @@ class AppSettings {
bool? mapShowMarkers, bool? mapShowMarkers,
bool? mapShowGuessedLocations, bool? mapShowGuessedLocations,
bool? enableMessageTracing, bool? enableMessageTracing,
ClockFormat? clockFormat,
Object? mapCacheBounds = _unset, Object? mapCacheBounds = _unset,
int? mapCacheMinZoom, int? mapCacheMinZoom,
int? mapCacheMaxZoom, int? mapCacheMaxZoom,
@ -423,6 +454,7 @@ class AppSettings {
mapShowGuessedLocations: mapShowGuessedLocations:
mapShowGuessedLocations ?? this.mapShowGuessedLocations, mapShowGuessedLocations ?? this.mapShowGuessedLocations,
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing, enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
clockFormat: clockFormat ?? this.clockFormat,
mapCacheBounds: mapCacheBounds == _unset mapCacheBounds: mapCacheBounds == _unset
? this.mapCacheBounds ? this.mapCacheBounds
: mapCacheBounds as Map<String, double>?, : mapCacheBounds as Map<String, double>?,

@ -22,6 +22,7 @@ import '../l10n/l10n.dart';
import '../models/channel.dart'; import '../models/channel.dart';
import '../models/channel_message.dart'; import '../models/channel_message.dart';
import '../models/translation_support.dart'; import '../models/translation_support.dart';
import '../models/app_settings.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart'; import '../services/chat_text_scale_service.dart';
import '../services/translation_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/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart'; import '../widgets/gif_picker.dart';
import '../widgets/message_translation_button.dart'; import '../widgets/message_translation_button.dart';
import '../widgets/message_status_icon.dart';
import '../widgets/radio_stats_entry.dart'; import '../widgets/radio_stats_entry.dart';
import '../widgets/sync_progress_overlay.dart'; import '../widgets/sync_progress_overlay.dart';
import '../widgets/translated_message_content.dart'; import '../widgets/translated_message_content.dart';
@ -574,10 +574,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
), ),
if (gifId == null) const SizedBox(height: 4), if (gifId == null) const SizedBox(height: 4),
], ],
if (message.replyToMessageId != null) ...[
_buildReplyPreview(message, textScale),
const SizedBox(height: 8),
],
if (poi != null) if (poi != null)
_buildPoiMessage( _buildPoiMessage(
context, context,
@ -585,20 +581,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
isOutgoing, isOutgoing,
textScale, textScale,
message.senderName, 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) else if (gifId != null)
Stack( Stack(
@ -618,36 +600,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
.withValues(alpha: 0.6), .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 else
@ -670,31 +622,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
), ),
), ),
), ),
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), const SizedBox(height: 4),
_buildMetaRow(context, message, isOutgoing),
if (enableTracing && displayPath.isNotEmpty) ...[
const SizedBox(height: 2),
Padding( Padding(
padding: gifId != null padding: gifId != null
? const EdgeInsets.symmetric(horizontal: 8) ? const EdgeInsets.symmetric(horizontal: 8)
@ -713,66 +646,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
), ),
), ),
], ],
const SizedBox(height: 4),
Padding(
padding: gifId != null
? const EdgeInsets.only(
left: 8,
right: 8,
bottom: 4,
)
: 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],
),
),
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],
),
],
],
),
),
],
], ],
), ),
), ),
@ -847,83 +720,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
Widget _buildReplyPreview(ChannelMessage message, double textScale) {
final connector = context.read<MeshCoreConnector>();
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) { Widget _buildReactionsDisplay(ChannelMessage message) {
return Wrap( return Wrap(
spacing: 6, spacing: 6,
@ -1427,19 +1223,78 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
void _ensureDateFormats(BuildContext context) { void _ensureDateFormats(BuildContext context) {
final locale = Localizations.localeOf(context).toString(); final locale = Localizations.localeOf(context).toString();
if (locale != _cachedFormatLocale) { final clock = context.read<AppSettingsService>().settings.clockFormat;
_cachedFormatLocale = locale; final systemUses24 = MediaQuery.of(context).alwaysUse24HourFormat;
_hmFormat = DateFormat.Hm(locale); 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); _mdFormat = DateFormat.Md(locale);
} }
} }
String _formatTime(BuildContext context, DateTime time) { String _formatTime(BuildContext context, DateTime time) {
_ensureDateFormats(context); _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); 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) => bool _isSameDay(DateTime a, DateTime b) =>
a.year == b.year && a.month == b.month && a.day == b.day; a.year == b.year && a.month == b.month && a.day == b.day;

@ -7,6 +7,7 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
@ -22,6 +23,7 @@ import '../models/channel_message.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../l10n/contact_localization.dart'; import '../l10n/contact_localization.dart';
import '../models/message.dart'; import '../models/message.dart';
import '../models/app_settings.dart';
import '../models/path_history.dart'; import '../models/path_history.dart';
import '../models/translation_support.dart'; import '../models/translation_support.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
@ -1993,7 +1995,7 @@ class _MessageBubble extends StatelessWidget {
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
Text( Text(
_formatTime(message.timestamp), _formatTime(context, message.timestamp),
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: metaColor, color: metaColor,
@ -2254,9 +2256,16 @@ class _MessageBubble extends StatelessWidget {
return Icon(icon, size: 12, color: color); return Icon(icon, size: 12, color: color);
} }
String _formatTime(DateTime time) { String _formatTime(BuildContext context, DateTime time) {
final hour = time.hour.toString().padLeft(2, '0'); final locale = Localizations.localeOf(context).toString();
final minute = time.minute.toString().padLeft(2, '0'); final clock = context.read<AppSettingsService>().settings.clockFormat;
return '$hour:$minute'; 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);
} }
} }

@ -75,6 +75,16 @@ class AppSettingsView extends StatelessWidget {
onTap: () => _showThemeModeDialog(context, settingsService), onTap: () => _showThemeModeDialog(context, settingsService),
), ),
const Divider(height: 1), 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( ListTile(
leading: const Icon(Icons.language_outlined), leading: const Icon(Icons.language_outlined),
title: Text(context.l10n.appSettings_language), 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<ClockFormat>(
groupValue: settingsService.settings.clockFormat,
onChanged: (value) {
if (value != null) {
settingsService.setClockFormat(value);
Navigator.pop(context);
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<ClockFormat>(
title: Text(context.l10n.appSettings_clockSystem),
value: ClockFormat.system,
),
RadioListTile<ClockFormat>(
title: Text(context.l10n.appSettings_clock12h),
value: ClockFormat.twelveHour,
),
RadioListTile<ClockFormat>(
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) { String _languageLabel(BuildContext context, String? languageCode) {
switch (languageCode) { switch (languageCode) {
case 'en': case 'en':

@ -100,6 +100,10 @@ class AppSettingsService extends ChangeNotifier {
await updateSettings(_settings.copyWith(enableMessageTracing: value)); await updateSettings(_settings.copyWith(enableMessageTracing: value));
} }
Future<void> setClockFormat(ClockFormat value) async {
await updateSettings(_settings.copyWith(clockFormat: value));
}
Future<void> setMapCacheBounds(Map<String, double>? value) async { Future<void> setMapCacheBounds(Map<String, double>? value) async {
await updateSettings(_settings.copyWith(mapCacheBounds: value)); await updateSettings(_settings.copyWith(mapCacheBounds: value));
} }

@ -2,102 +2,187 @@
"bg": [ "bg": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"de": [ "de": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"es": [ "es": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"fr": [ "fr": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"hu": [ "hu": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"it": [ "it": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"ja": [ "ja": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"ko": [ "ko": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"nl": [ "nl": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"pl": [ "pl": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"pt": [ "pt": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"ru": [ "ru": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"sk": [ "sk": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"sl": [ "sl": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"sv": [ "sv": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"uk": [ "uk": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
], ],
"zh": [ "zh": [
"settings_messageSettings", "settings_messageSettings",
"settings_messageSettingsSubtitle", "settings_messageSettingsSubtitle",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
"appSettings_clock24h",
"channel_heardTooltip",
"snrIndicator_noNeighbors" "snrIndicator_noNeighbors"
] ]
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.