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
}
// 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<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(
List<ChannelMessage> messages,
ReactionInfo reactionInfo,

@ -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",

@ -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:

@ -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 => 'Успехи';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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 => '成功事例';

@ -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 => '성공 사례';

@ -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';

@ -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';

@ -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';

@ -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 => 'успешно';

@ -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';

@ -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';

@ -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';

@ -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 => 'Успішно';

@ -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 => '成功';

@ -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 = {
'А': 'A',
'В': 'B',
@ -88,6 +103,7 @@ class AppSettings {
final bool mapShowMarkers;
final bool mapShowGuessedLocations;
final bool enableMessageTracing;
final ClockFormat clockFormat;
final Map<String, double>? 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<String, double>?,

@ -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<ChannelChatScreen> {
),
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<ChannelChatScreen> {
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<ChannelChatScreen> {
.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,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),
_buildMetaRow(context, message, isOutgoing),
if (enableTracing && displayPath.isNotEmpty) ...[
const SizedBox(height: 2),
Padding(
padding: gifId != null
? 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) {
return Wrap(
spacing: 6,
@ -1427,19 +1223,78 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
void _ensureDateFormats(BuildContext context) {
final locale = Localizations.localeOf(context).toString();
if (locale != _cachedFormatLocale) {
_cachedFormatLocale = locale;
_hmFormat = DateFormat.Hm(locale);
final clock = context.read<AppSettingsService>().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;

@ -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<AppSettingsService>().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);
}
}

@ -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<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) {
switch (languageCode) {
case 'en':

@ -100,6 +100,10 @@ class AppSettingsService extends ChangeNotifier {
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 {
await updateSettings(_settings.copyWith(mapCacheBounds: value));
}

@ -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"
]
}

Loading…
Cancel
Save

Powered by TurnKey Linux.