feat: Add new contact and message tracking features, including unread status and removal functionality

This commit is contained in:
Janez T
2025-10-15 21:49:33 +02:00
parent 2849ab92aa
commit 6be182e20a
13 changed files with 463 additions and 48 deletions

View File

@@ -6,6 +6,7 @@ import 'package:latlong2/latlong.dart';
import '../../models/contact.dart';
import '../../models/room_login_state.dart';
import '../../providers/connection_provider.dart';
import '../../providers/contacts_provider.dart';
import '../../providers/map_provider.dart';
import 'direct_message_sheet.dart';
import 'room_login_sheet.dart';
@@ -66,7 +67,22 @@ class ContactTile extends StatelessWidget {
color: Colors.white,
),
),
// Room login status indicator badge
// New contact indicator badge (top-right)
if (contact.isNew)
Positioned(
top: 0,
right: 0,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
),
// Room login status indicator badge (bottom-right)
if (contact.type == ContactType.room && roomLoginState != null)
Positioned(
bottom: 0,
@@ -361,6 +377,62 @@ class ContactTile extends StatelessWidget {
);
}
void _showDeleteConfirmation(BuildContext context, Contact contact) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Contact'),
content: Text(
'Are you sure you want to delete "${contact.displayName}"?\n\n'
'This will remove the contact from both the app and the companion radio device.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
Navigator.pop(context); // Close confirmation dialog
Navigator.pop(context); // Close contact details sheet
await _deleteContact(context, contact);
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Delete'),
),
],
),
);
}
Future<void> _deleteContact(BuildContext context, Contact contact) async {
final connectionProvider = context.read<ConnectionProvider>();
final contactsProvider = context.read<ContactsProvider>();
try {
// Show loading toast
ToastLogger.info(context, 'Removing ${contact.displayName}...');
// Remove contact from provider (which will also remove from device)
await contactsProvider.removeContact(
contact.publicKeyHex,
onRemoveFromDevice: (publicKey) async {
if (connectionProvider.deviceInfo.isConnected) {
await connectionProvider.removeContact(publicKey);
}
},
);
if (context.mounted) {
ToastLogger.success(context, 'Contact "${contact.displayName}" removed');
}
} catch (e) {
if (context.mounted) {
ToastLogger.error(context, 'Failed to remove contact: $e');
}
}
}
void _showContactDetails(BuildContext context, Contact contact) {
// Get room login state
final connectionProvider = context.read<ConnectionProvider>();
@@ -684,6 +756,23 @@ class ContactTile extends StatelessWidget {
),
),
],
// Delete Contact button (for all contact types except Public Channel)
if (contact.advName != 'Public Channel') ...[
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => _showDeleteConfirmation(context, contact),
icon: const Icon(Icons.delete_outline),
label: const Text('Delete Contact'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: const BorderSide(color: Colors.red),
foregroundColor: Colors.red,
),
),
),
],
],
),
),

View File

@@ -1,8 +1,11 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../models/contact.dart';
import '../../models/message.dart';
import '../../providers/connection_provider.dart';
import '../../providers/messages_provider.dart';
import '../../utils/toast_logger.dart';
class DirectMessageSheet extends StatefulWidget {
@@ -44,6 +47,7 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
if (text.isEmpty) return;
final connectionProvider = context.read<ConnectionProvider>();
final messagesProvider = context.read<MessagesProvider>();
if (!connectionProvider.deviceInfo.isConnected) {
if (!mounted) return;
@@ -52,13 +56,44 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
}
try {
// Create message ID
final messageId = '${DateTime.now().millisecondsSinceEpoch}_dm_sent';
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
// Get current device's public key (first 6 bytes)
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);
// Create sent message object with recipient public key for retry support
final sentMessage = Message(
id: messageId,
messageType: MessageType.contact,
senderPublicKeyPrefix: senderPublicKeyPrefix,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: timestamp,
text: text,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
recipientPublicKey: widget.contact.publicKey, // Store recipient for retry
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
// Send direct message to contact (include contact for path logging)
await connectionProvider.sendTextMessage(
final sentSuccessfully = await connectionProvider.sendTextMessage(
contactPublicKey: widget.contact.publicKey,
text: text,
messageId: messageId, // Pass message ID for tracking
contact: widget.contact,
);
if (!sentSuccessfully) {
// Mark message as failed if sending failed
messagesProvider.markMessageFailed(messageId);
}
_textController.clear();
_focusNode.unfocus();