mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Review MeshCore payload status
This commit is contained in:
@@ -213,6 +213,22 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
.where((message) => _isMessageForDestination(message, contact))
|
.where((message) => _isMessageForDestination(message, contact))
|
||||||
.length;
|
.length;
|
||||||
|
|
||||||
|
DateTime? getLastActivityForDestination(Contact contact) {
|
||||||
|
DateTime? latest;
|
||||||
|
|
||||||
|
for (final message in _messages) {
|
||||||
|
if (!_isMessageForDestination(message, contact)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latest == null || message.sentAt.isAfter(latest)) {
|
||||||
|
latest = message.sentAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
int getUnreadCountForDestination(Contact contact) => _messages
|
int getUnreadCountForDestination(Contact contact) => _messages
|
||||||
.where(
|
.where(
|
||||||
(message) =>
|
(message) =>
|
||||||
@@ -904,7 +920,8 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
|
|
||||||
final matchesDestination = switch (destinationType) {
|
final matchesDestination = switch (destinationType) {
|
||||||
'channel' => _isChannelMessageForContact(message, contact),
|
'channel' => _isChannelMessageForContact(message, contact),
|
||||||
'contact' || 'room' => contact != null && _isMessageForDestination(message, contact),
|
'contact' ||
|
||||||
|
'room' => contact != null && _isMessageForDestination(message, contact),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -665,12 +665,32 @@ class _ChannelActivityCard extends StatelessWidget {
|
|||||||
required this.onNavigateToMessages,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
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 channelIdx = channel.publicKey.length > 1 ? channel.publicKey[1] : 0;
|
||||||
final channelMessages = messagesProvider.getMessagesForChannel(channelIdx)
|
final channelMessages = messagesProvider.getMessagesForChannel(channelIdx)
|
||||||
..sort((a, b) => b.sentAt.compareTo(a.sentAt));
|
..sort((a, b) => b.sentAt.compareTo(a.sentAt));
|
||||||
|
final lastActivityAt = messagesProvider.getLastActivityForDestination(
|
||||||
|
channel,
|
||||||
|
);
|
||||||
|
final activityLabel = lastActivityAt == null
|
||||||
|
? null
|
||||||
|
: _formatRelativeTime(context, lastActivityAt);
|
||||||
final participantNames = <String>[];
|
final participantNames = <String>[];
|
||||||
for (final message in channelMessages) {
|
for (final message in channelMessages) {
|
||||||
final senderName = message.senderName?.trim();
|
final senderName = message.senderName?.trim();
|
||||||
@@ -688,12 +708,19 @@ class _ChannelActivityCard extends StatelessWidget {
|
|||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [
|
||||||
colorScheme.surfaceContainerLow,
|
colorScheme.surfaceContainerLow,
|
||||||
colorScheme.tertiaryContainer.withValues(alpha: 0.45),
|
colorScheme.surfaceContainerHighest.withValues(alpha: 0.9),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: colorScheme.outlineVariant.withValues(alpha: 0.35),
|
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(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
@@ -715,23 +742,35 @@ class _ChannelActivityCard extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
ContactAvatar(contact: channel, radius: 20),
|
ContactAvatar(contact: channel, radius: 24),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
channel.displayName,
|
channel.displayName,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context).textTheme.titleMedium
|
style: titleStyle,
|
||||||
?.copyWith(
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
fontSize: 15,
|
|
||||||
height: 1.05,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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),
|
const SizedBox(height: 8),
|
||||||
if (participantNames.isNotEmpty)
|
if (participantNames.isNotEmpty)
|
||||||
_ExpandableParticipantStack(
|
_ExpandableParticipantStack(
|
||||||
|
|||||||
@@ -772,7 +772,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
Icons.message,
|
Icons.message,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
),
|
),
|
||||||
text: AppLocalizations.of(context)!.messages,
|
text: 'Chat',
|
||||||
);
|
);
|
||||||
case _HomeTab.contacts:
|
case _HomeTab.contacts:
|
||||||
return Tab(
|
return Tab(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
@@ -5,6 +7,7 @@ import 'package:latlong2/latlong.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
import '../providers/connection_provider.dart';
|
||||||
import '../providers/contacts_provider.dart';
|
import '../providers/contacts_provider.dart';
|
||||||
import '../services/mesh_map_nodes_service.dart';
|
import '../services/mesh_map_nodes_service.dart';
|
||||||
|
|
||||||
@@ -28,6 +31,7 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
|||||||
String? _error;
|
String? _error;
|
||||||
double _currentZoom = _fallbackZoom;
|
double _currentZoom = _fallbackZoom;
|
||||||
LatLng? _myLocation;
|
LatLng? _myLocation;
|
||||||
|
final Set<String> _addingRepeaters = <String>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -164,6 +168,13 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showRepeaterDetails(_MapRepeater repeater) {
|
void _showRepeaterDetails(_MapRepeater repeater) {
|
||||||
|
final isAdding = _addingRepeaters.contains(repeater.publicKey);
|
||||||
|
final canAdd = !repeater.isFromContacts;
|
||||||
|
final isConnected = context
|
||||||
|
.read<ConnectionProvider>()
|
||||||
|
.deviceInfo
|
||||||
|
.isConnected;
|
||||||
|
|
||||||
showModalBottomSheet<void>(
|
showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
showDragHandle: true,
|
showDragHandle: true,
|
||||||
@@ -192,6 +203,40 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
|||||||
'${repeater.latitude.toStringAsFixed(5)}, ${repeater.longitude.toStringAsFixed(5)}',
|
'${repeater.latitude.toStringAsFixed(5)}, ${repeater.longitude.toStringAsFixed(5)}',
|
||||||
),
|
),
|
||||||
_InfoLine(label: 'Source', value: repeater.sourceLabel),
|
_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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final contactsProvider = context.watch<ContactsProvider>();
|
final contactsProvider = context.watch<ContactsProvider>();
|
||||||
|
|||||||
@@ -36,10 +36,9 @@ class ContactTile extends StatelessWidget {
|
|||||||
this.onNavigateToMessages,
|
this.onNavigateToMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Get localized time since last seen
|
String _getLocalizedRelativeTime(BuildContext context, DateTime when) {
|
||||||
String _getLocalizedTimeSinceLastSeen(BuildContext context) {
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final diff = DateTime.now().difference(contact.lastSeenTime);
|
final diff = DateTime.now().difference(when);
|
||||||
|
|
||||||
if (diff.inMinutes < 1) return l10n.justNow;
|
if (diff.inMinutes < 1) return l10n.justNow;
|
||||||
if (diff.inMinutes < 60) return l10n.minutesAgo(diff.inMinutes);
|
if (diff.inMinutes < 60) return l10n.minutesAgo(diff.inMinutes);
|
||||||
@@ -50,6 +49,8 @@ class ContactTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isChannel = contact.type == ContactType.channel;
|
final isChannel = contact.type == ContactType.channel;
|
||||||
|
final isRoom = contact.type == ContactType.room;
|
||||||
|
final isRoomOrChannel = isRoom || isChannel;
|
||||||
final location = contact.displayLocation;
|
final location = contact.displayLocation;
|
||||||
// Calculate distance if both positions are available
|
// Calculate distance if both positions are available
|
||||||
String? distanceText;
|
String? distanceText;
|
||||||
@@ -68,6 +69,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
|
|
||||||
// Get room login state if this is a room
|
// Get room login state if this is a room
|
||||||
final connectionProvider = context.watch<ConnectionProvider>();
|
final connectionProvider = context.watch<ConnectionProvider>();
|
||||||
|
final messagesProvider = context.watch<MessagesProvider>();
|
||||||
final isPingInProgress = connectionProvider.isPingInProgress(
|
final isPingInProgress = connectionProvider.isPingInProgress(
|
||||||
contact.publicKey,
|
contact.publicKey,
|
||||||
);
|
);
|
||||||
@@ -109,16 +111,20 @@ class ContactTile extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
letterSpacing: -0.3,
|
letterSpacing: -0.3,
|
||||||
);
|
);
|
||||||
final timeAgoText = _getLocalizedTimeSinceLastSeen(context);
|
final lastActivityAt = isRoomOrChannel
|
||||||
|
? messagesProvider.getLastActivityForDestination(contact)
|
||||||
|
: null;
|
||||||
|
final timeAgoText = _getLocalizedRelativeTime(
|
||||||
|
context,
|
||||||
|
lastActivityAt ?? contact.lastSeenTime,
|
||||||
|
);
|
||||||
final timeAgoStyle = Theme.of(context).textTheme.labelSmall?.copyWith(
|
final timeAgoStyle = Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
color: contact.isRecentlySeen
|
color: contact.isRecentlySeen
|
||||||
? colorScheme.primary
|
? colorScheme.primary
|
||||||
: colorScheme.onSurfaceVariant,
|
: colorScheme.onSurfaceVariant,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
final Widget? subtitleWidget = isChannel
|
final Widget subtitleWidget = Column(
|
||||||
? null
|
|
||||||
: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (location != null) ...[
|
if (location != null) ...[
|
||||||
@@ -239,7 +245,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
if (!isChannel)
|
if (!isRoomOrChannel || lastActivityAt != null)
|
||||||
Text(timeAgoText, style: timeAgoStyle),
|
Text(timeAgoText, style: timeAgoStyle),
|
||||||
if (isPingInProgress) ...[
|
if (isPingInProgress) ...[
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
@@ -254,7 +260,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
?subtitleWidget,
|
subtitleWidget,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user