Review MeshCore payload status

This commit is contained in:
Janez T
2026-03-10 11:05:54 +01:00
parent 8085b89c07
commit 818b9dbfb2
5 changed files with 206 additions and 46 deletions

View File

@@ -665,12 +665,32 @@ class _ChannelActivityCard extends StatelessWidget {
required this.onNavigateToMessages,
});
String _formatRelativeTime(BuildContext context, DateTime when) {
final l10n = AppLocalizations.of(context)!;
final diff = DateTime.now().difference(when);
if (diff.inMinutes < 1) return l10n.justNow;
if (diff.inMinutes < 60) return l10n.minutesAgo(diff.inMinutes);
if (diff.inHours < 24) return l10n.hoursAgo(diff.inHours);
return l10n.daysAgo(diff.inDays);
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final titleStyle = Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
);
final channelIdx = channel.publicKey.length > 1 ? channel.publicKey[1] : 0;
final channelMessages = messagesProvider.getMessagesForChannel(channelIdx)
..sort((a, b) => b.sentAt.compareTo(a.sentAt));
final lastActivityAt = messagesProvider.getLastActivityForDestination(
channel,
);
final activityLabel = lastActivityAt == null
? null
: _formatRelativeTime(context, lastActivityAt);
final participantNames = <String>[];
for (final message in channelMessages) {
final senderName = message.senderName?.trim();
@@ -688,12 +708,19 @@ class _ChannelActivityCard extends StatelessWidget {
end: Alignment.bottomRight,
colors: [
colorScheme.surfaceContainerLow,
colorScheme.tertiaryContainer.withValues(alpha: 0.45),
colorScheme.surfaceContainerHighest.withValues(alpha: 0.9),
],
),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.35),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.045),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
@@ -715,22 +742,34 @@ class _ChannelActivityCard extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ContactAvatar(contact: channel, radius: 20),
ContactAvatar(contact: channel, radius: 24),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
channel.displayName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(
fontWeight: FontWeight.w800,
fontSize: 15,
height: 1.05,
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
channel.displayName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: titleStyle,
),
),
const SizedBox(width: 8),
if (activityLabel != null)
Text(
activityLabel,
style: Theme.of(context).textTheme.labelSmall
?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
if (participantNames.isNotEmpty)

View File

@@ -772,7 +772,7 @@ class _HomeScreenState extends State<HomeScreen>
Icons.message,
unreadCount,
),
text: AppLocalizations.of(context)!.messages,
text: 'Chat',
);
case _HomeTab.contacts:
return Tab(

View File

@@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart' as flutter_map;
import 'package:geolocator/geolocator.dart';
@@ -5,6 +7,7 @@ import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import '../models/contact.dart';
import '../providers/connection_provider.dart';
import '../providers/contacts_provider.dart';
import '../services/mesh_map_nodes_service.dart';
@@ -28,6 +31,7 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
String? _error;
double _currentZoom = _fallbackZoom;
LatLng? _myLocation;
final Set<String> _addingRepeaters = <String>{};
@override
void initState() {
@@ -164,6 +168,13 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
}
void _showRepeaterDetails(_MapRepeater repeater) {
final isAdding = _addingRepeaters.contains(repeater.publicKey);
final canAdd = !repeater.isFromContacts;
final isConnected = context
.read<ConnectionProvider>()
.deviceInfo
.isConnected;
showModalBottomSheet<void>(
context: context,
showDragHandle: true,
@@ -192,6 +203,40 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
'${repeater.latitude.toStringAsFixed(5)}, ${repeater.longitude.toStringAsFixed(5)}',
),
_InfoLine(label: 'Source', value: repeater.sourceLabel),
const SizedBox(height: 12),
if (canAdd)
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: !isConnected || isAdding
? null
: () async {
Navigator.of(context).pop();
await _addRepeaterToContacts(repeater);
},
icon: isAdding
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.person_add_alt_1),
label: Text(
!isConnected
? 'Connect device to add'
: 'Add to contacts',
),
),
)
else
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: null,
icon: const Icon(Icons.check_circle),
label: const Text('Already in contacts'),
),
),
],
),
),
@@ -199,6 +244,59 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
);
}
Future<void> _addRepeaterToContacts(_MapRepeater repeater) async {
if (_addingRepeaters.contains(repeater.publicKey)) {
return;
}
final connectionProvider = context.read<ConnectionProvider>();
if (!connectionProvider.deviceInfo.isConnected) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connect to a device before adding contacts'),
),
);
return;
}
setState(() {
_addingRepeaters.add(repeater.publicKey);
});
try {
await connectionProvider.getContact(_hexToBytes(repeater.publicKey));
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${repeater.name} added to contacts')),
);
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to add ${repeater.name}: $error')),
);
} finally {
if (mounted) {
setState(() {
_addingRepeaters.remove(repeater.publicKey);
});
}
}
}
Uint8List _hexToBytes(String hex) {
final normalized = hex.replaceAll(':', '').trim().toLowerCase();
return Uint8List.fromList(
List<int>.generate(
normalized.length ~/ 2,
(index) => int.parse(
normalized.substring(index * 2, index * 2 + 2),
radix: 16,
),
),
);
}
@override
Widget build(BuildContext context) {
final contactsProvider = context.watch<ContactsProvider>();