mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Display unread counts per channel
This commit is contained in:
@@ -62,6 +62,8 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// Navigation state for message highlighting/scrolling
|
// Navigation state for message highlighting/scrolling
|
||||||
String? _targetMessageId;
|
String? _targetMessageId;
|
||||||
|
String? _targetDestinationType;
|
||||||
|
String? _targetRecipientPublicKeyHex;
|
||||||
|
|
||||||
// Helper function to compare Uint8List for equality
|
// Helper function to compare Uint8List for equality
|
||||||
bool _listEquals(Uint8List a, Uint8List b) {
|
bool _listEquals(Uint8List a, Uint8List b) {
|
||||||
@@ -183,11 +185,25 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? get targetDestinationType => _targetDestinationType;
|
||||||
|
String? get targetRecipientPublicKeyHex => _targetRecipientPublicKeyHex;
|
||||||
|
|
||||||
|
void navigateToDestination(String type, {String? recipientPublicKeyHex}) {
|
||||||
|
_targetDestinationType = type;
|
||||||
|
_targetRecipientPublicKeyHex = recipientPublicKeyHex;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
/// Clear message navigation state
|
/// Clear message navigation state
|
||||||
void clearMessageNavigation() {
|
void clearMessageNavigation() {
|
||||||
_targetMessageId = null;
|
_targetMessageId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearDestinationNavigation() {
|
||||||
|
_targetDestinationType = null;
|
||||||
|
_targetRecipientPublicKeyHex = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get count of unread messages (excluding sent messages and system messages)
|
/// Get count of unread messages (excluding sent messages and system messages)
|
||||||
int get unreadCount => _messages
|
int get unreadCount => _messages
|
||||||
.where((m) => !m.isRead && !m.isSentMessage && !m.isSystemMessage)
|
.where((m) => !m.isRead && !m.isSentMessage && !m.isSystemMessage)
|
||||||
|
|||||||
@@ -13,8 +13,13 @@ import '../widgets/contacts/add_channel_dialog.dart';
|
|||||||
|
|
||||||
class ContactsTab extends StatefulWidget {
|
class ContactsTab extends StatefulWidget {
|
||||||
final VoidCallback? onNavigateToMap;
|
final VoidCallback? onNavigateToMap;
|
||||||
|
final VoidCallback? onNavigateToMessages;
|
||||||
|
|
||||||
const ContactsTab({super.key, this.onNavigateToMap});
|
const ContactsTab({
|
||||||
|
super.key,
|
||||||
|
this.onNavigateToMap,
|
||||||
|
this.onNavigateToMessages,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ContactsTab> createState() => _ContactsTabState();
|
State<ContactsTab> createState() => _ContactsTabState();
|
||||||
@@ -285,6 +290,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
calculateDistance: _calculateDistanceInMeters,
|
calculateDistance: _calculateDistanceInMeters,
|
||||||
formatDistance: _formatDistance,
|
formatDistance: _formatDistance,
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
onNavigateToMap: widget.onNavigateToMap,
|
||||||
|
onNavigateToMessages: widget.onNavigateToMessages,
|
||||||
messageCount: messagesProvider
|
messageCount: messagesProvider
|
||||||
.getMessageCountForDestination(contact),
|
.getMessageCountForDestination(contact),
|
||||||
unreadMessageCount: messagesProvider
|
unreadMessageCount: messagesProvider
|
||||||
@@ -308,6 +314,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
calculateDistance: _calculateDistanceInMeters,
|
calculateDistance: _calculateDistanceInMeters,
|
||||||
formatDistance: _formatDistance,
|
formatDistance: _formatDistance,
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
onNavigateToMap: widget.onNavigateToMap,
|
||||||
|
onNavigateToMessages: widget.onNavigateToMessages,
|
||||||
messageCount: messagesProvider
|
messageCount: messagesProvider
|
||||||
.getMessageCountForDestination(contact),
|
.getMessageCountForDestination(contact),
|
||||||
unreadMessageCount: messagesProvider
|
unreadMessageCount: messagesProvider
|
||||||
@@ -331,6 +338,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
calculateDistance: _calculateDistanceInMeters,
|
calculateDistance: _calculateDistanceInMeters,
|
||||||
formatDistance: _formatDistance,
|
formatDistance: _formatDistance,
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
onNavigateToMap: widget.onNavigateToMap,
|
||||||
|
onNavigateToMessages: widget.onNavigateToMessages,
|
||||||
messageCount: messagesProvider
|
messageCount: messagesProvider
|
||||||
.getMessageCountForDestination(contact),
|
.getMessageCountForDestination(contact),
|
||||||
unreadMessageCount: messagesProvider
|
unreadMessageCount: messagesProvider
|
||||||
@@ -354,6 +362,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
calculateDistance: _calculateDistanceInMeters,
|
calculateDistance: _calculateDistanceInMeters,
|
||||||
formatDistance: _formatDistance,
|
formatDistance: _formatDistance,
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
onNavigateToMap: widget.onNavigateToMap,
|
||||||
|
onNavigateToMessages: widget.onNavigateToMessages,
|
||||||
messageCount: messagesProvider
|
messageCount: messagesProvider
|
||||||
.getMessageCountForDestination(contact),
|
.getMessageCountForDestination(contact),
|
||||||
unreadMessageCount: messagesProvider
|
unreadMessageCount: messagesProvider
|
||||||
|
|||||||
@@ -688,6 +688,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
onNavigateToMap: _isMapEnabled
|
onNavigateToMap: _isMapEnabled
|
||||||
? () => _navigateToTab(_HomeTab.map)
|
? () => _navigateToTab(_HomeTab.map)
|
||||||
: null,
|
: null,
|
||||||
|
onNavigateToMessages: () => _navigateToTab(_HomeTab.messages),
|
||||||
);
|
);
|
||||||
case _HomeTab.sensors:
|
case _HomeTab.sensors:
|
||||||
return const SensorsTab();
|
return const SensorsTab();
|
||||||
|
|||||||
@@ -128,11 +128,20 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
void _checkForNavigationRequest() {
|
void _checkForNavigationRequest() {
|
||||||
final messagesProvider = context.read<MessagesProvider>();
|
final messagesProvider = context.read<MessagesProvider>();
|
||||||
final targetMessageId = messagesProvider.targetMessageId;
|
final targetMessageId = messagesProvider.targetMessageId;
|
||||||
|
final targetDestinationType = messagesProvider.targetDestinationType;
|
||||||
|
|
||||||
if (targetMessageId != null) {
|
if (targetMessageId != null) {
|
||||||
_scrollToMessage(targetMessageId);
|
_scrollToMessage(targetMessageId);
|
||||||
messagesProvider.clearMessageNavigation();
|
messagesProvider.clearMessageNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetDestinationType != null) {
|
||||||
|
_applyPendingDestination(
|
||||||
|
type: targetDestinationType,
|
||||||
|
recipientPublicKeyHex: messagesProvider.targetRecipientPublicKeyHex,
|
||||||
|
);
|
||||||
|
messagesProvider.clearDestinationNavigation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scrollToMessage(String messageId) {
|
void _scrollToMessage(String messageId) {
|
||||||
@@ -304,6 +313,23 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _applyPendingDestination({
|
||||||
|
required String type,
|
||||||
|
String? recipientPublicKeyHex,
|
||||||
|
}) async {
|
||||||
|
Contact? recipient;
|
||||||
|
if (recipientPublicKeyHex != null) {
|
||||||
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
|
recipient = contactsProvider.contacts.where((contact) {
|
||||||
|
return contact.publicKeyHex == recipientPublicKeyHex;
|
||||||
|
}).firstOrNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _onRecipientSelected(type, recipient);
|
||||||
|
if (!mounted) return;
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _replyToMessage(Message message) async {
|
Future<void> _replyToMessage(Message message) async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final contactsProvider = context.read<ContactsProvider>();
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
@@ -351,7 +377,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
|
|
||||||
/// Get icon for current destination type
|
/// Get icon for current destination type
|
||||||
IconData _getDestinationIcon() {
|
IconData _getDestinationIcon() {
|
||||||
if (_destinationType ==
|
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||||
|
return Icons.all_inbox;
|
||||||
|
} else if (_destinationType ==
|
||||||
MessageDestinationPreferences.destinationTypeChannel) {
|
MessageDestinationPreferences.destinationTypeChannel) {
|
||||||
return Icons.public;
|
return Icons.public;
|
||||||
} else if (_destinationType ==
|
} else if (_destinationType ==
|
||||||
@@ -363,6 +391,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _getDestinationLabel() {
|
String _getDestinationLabel() {
|
||||||
|
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||||
|
return AppLocalizations.of(context)!.showAll;
|
||||||
|
}
|
||||||
if (_destinationType ==
|
if (_destinationType ==
|
||||||
MessageDestinationPreferences.destinationTypeChannel &&
|
MessageDestinationPreferences.destinationTypeChannel &&
|
||||||
_selectedRecipient != null) {
|
_selectedRecipient != null) {
|
||||||
@@ -388,6 +419,12 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ToastLogger.error(context, 'Select a channel, contact, or room first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check destination type and send accordingly
|
// Check destination type and send accordingly
|
||||||
if (_destinationType ==
|
if (_destinationType ==
|
||||||
@@ -1152,6 +1189,15 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.search),
|
||||||
|
title: const Text('Search messages'),
|
||||||
|
onTap: () async {
|
||||||
|
await _runAfterSheetDismissal(sheetContext, () async {
|
||||||
|
_showFilteredMessageSearch();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.add_location_alt),
|
leading: const Icon(Icons.add_location_alt),
|
||||||
title: Text(AppLocalizations.of(context)!.sendSarMarker),
|
title: Text(AppLocalizations.of(context)!.sendSarMarker),
|
||||||
@@ -1219,6 +1265,116 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showFilteredMessageSearch() {
|
||||||
|
final messagesProvider = context.read<MessagesProvider>();
|
||||||
|
final scopedMessages = _getFilteredMessages(messagesProvider);
|
||||||
|
final searchController = TextEditingController();
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (sheetContext) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setModalState) {
|
||||||
|
final query = searchController.text.trim().toLowerCase();
|
||||||
|
final matches = query.isEmpty
|
||||||
|
? scopedMessages
|
||||||
|
: scopedMessages.where((message) {
|
||||||
|
return message.text.toLowerCase().contains(query) ||
|
||||||
|
(message.senderName?.toLowerCase().contains(query) ??
|
||||||
|
false);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(sheetContext).viewInsets.bottom,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: MediaQuery.of(sheetContext).size.height * 0.72,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||||
|
child: TextField(
|
||||||
|
controller: searchController,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search in current filter',
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
suffixIcon: searchController.text.isEmpty
|
||||||
|
? null
|
||||||
|
: IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
searchController.clear();
|
||||||
|
setModalState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (_) => setModalState(() {}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: matches.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
query.isEmpty
|
||||||
|
? 'No messages in this filter'
|
||||||
|
: 'No matches in this filter',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.separated(
|
||||||
|
itemCount: matches.length,
|
||||||
|
separatorBuilder: (_, _) =>
|
||||||
|
const Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final message = matches[index];
|
||||||
|
final title =
|
||||||
|
message.senderName?.trim().isNotEmpty ==
|
||||||
|
true
|
||||||
|
? message.senderName!
|
||||||
|
: message.isSentMessage
|
||||||
|
? 'You'
|
||||||
|
: _getDestinationLabel();
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
message.text,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
_scrollToMessage(message.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).whenComplete(searchController.dispose);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDestinationAvatar(BuildContext context) {
|
Widget _buildDestinationAvatar(BuildContext context) {
|
||||||
final recipient = _selectedRecipient;
|
final recipient = _selectedRecipient;
|
||||||
if (recipient != null) {
|
if (recipient != null) {
|
||||||
@@ -1484,18 +1640,19 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
List<Message> filteredMessages;
|
List<Message> filteredMessages;
|
||||||
|
|
||||||
// If channel destination is selected, filter by selected channel.
|
// If channel destination is selected, filter by selected channel.
|
||||||
if (_destinationType ==
|
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||||
|
filteredMessages = allMessages;
|
||||||
|
} else if (_destinationType ==
|
||||||
MessageDestinationPreferences.destinationTypeChannel) {
|
MessageDestinationPreferences.destinationTypeChannel) {
|
||||||
final selectedChannelIdx = _selectedRecipient?.publicKey[1] ?? 0;
|
final selectedChannelIdx = _selectedRecipient?.publicKey[1] ?? 0;
|
||||||
if (selectedChannelIdx == 0) {
|
|
||||||
// Public channel view keeps showing all messages (current app behavior).
|
|
||||||
filteredMessages = allMessages;
|
|
||||||
} else {
|
|
||||||
filteredMessages = allMessages
|
filteredMessages = allMessages
|
||||||
.where((message) => message.channelIdx == selectedChannelIdx)
|
.where(
|
||||||
|
(message) =>
|
||||||
|
message.isChannelMessage &&
|
||||||
|
(message.channelIdx ?? 0) == selectedChannelIdx,
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// If a contact or room is selected, filter by recipient/sender prefixes.
|
// If a contact or room is selected, filter by recipient/sender prefixes.
|
||||||
else if ((_destinationType ==
|
else if ((_destinationType ==
|
||||||
MessageDestinationPreferences.destinationTypeContact ||
|
MessageDestinationPreferences.destinationTypeContact ||
|
||||||
@@ -1503,6 +1660,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
MessageDestinationPreferences.destinationTypeRoom) &&
|
MessageDestinationPreferences.destinationTypeRoom) &&
|
||||||
_selectedRecipient != null) {
|
_selectedRecipient != null) {
|
||||||
filteredMessages = allMessages.where((message) {
|
filteredMessages = allMessages.where((message) {
|
||||||
|
if (!message.isContactMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Include messages sent TO this recipient
|
// Include messages sent TO this recipient
|
||||||
if (message.recipientPublicKey != null &&
|
if (message.recipientPublicKey != null &&
|
||||||
message.recipientPublicKey!.length >= 6 &&
|
message.recipientPublicKey!.length >= 6 &&
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class MessageDestinationPreferences {
|
|||||||
static const String _recipientPublicKeyKey = 'message_recipient_public_key';
|
static const String _recipientPublicKeyKey = 'message_recipient_public_key';
|
||||||
|
|
||||||
/// Destination types
|
/// Destination types
|
||||||
|
static const String destinationTypeAll = 'all';
|
||||||
static const String destinationTypeChannel = 'channel';
|
static const String destinationTypeChannel = 'channel';
|
||||||
static const String destinationTypeContact = 'contact';
|
static const String destinationTypeContact = 'contact';
|
||||||
static const String destinationTypeRoom = 'room';
|
static const String destinationTypeRoom = 'room';
|
||||||
@@ -24,14 +25,11 @@ class MessageDestinationPreferences {
|
|||||||
|
|
||||||
final publicKey = prefs.getString(_recipientPublicKeyKey);
|
final publicKey = prefs.getString(_recipientPublicKeyKey);
|
||||||
|
|
||||||
return {
|
return {'type': type, 'publicKey': ?publicKey};
|
||||||
'type': type,
|
|
||||||
'publicKey': ?publicKey,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save the selected destination
|
/// Save the selected destination
|
||||||
/// [type] - one of: destinationTypeChannel, destinationTypeContact, destinationTypeRoom
|
/// [type] - one of: destinationTypeAll, destinationTypeChannel, destinationTypeContact, destinationTypeRoom
|
||||||
/// [recipientPublicKey] - hex string of recipient's public key (required for contact/room)
|
/// [recipientPublicKey] - hex string of recipient's public key (required for contact/room)
|
||||||
static Future<void> setDestination(
|
static Future<void> setDestination(
|
||||||
String type, {
|
String type, {
|
||||||
@@ -58,6 +56,8 @@ class MessageDestinationPreferences {
|
|||||||
/// Get display name for destination type
|
/// Get display name for destination type
|
||||||
static String getDestinationTypeName(String type) {
|
static String getDestinationTypeName(String type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case destinationTypeAll:
|
||||||
|
return 'All';
|
||||||
case destinationTypeChannel:
|
case destinationTypeChannel:
|
||||||
return 'Channel';
|
return 'Channel';
|
||||||
case destinationTypeContact:
|
case destinationTypeContact:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import '../../models/room_login_state.dart';
|
|||||||
import '../../providers/connection_provider.dart';
|
import '../../providers/connection_provider.dart';
|
||||||
import '../../providers/contacts_provider.dart';
|
import '../../providers/contacts_provider.dart';
|
||||||
import '../../providers/map_provider.dart';
|
import '../../providers/map_provider.dart';
|
||||||
|
import '../../providers/messages_provider.dart';
|
||||||
|
import '../../services/message_destination_preferences.dart';
|
||||||
import 'contact_route_dialog.dart';
|
import 'contact_route_dialog.dart';
|
||||||
import 'room_login_sheet.dart';
|
import 'room_login_sheet.dart';
|
||||||
import '../common/contact_avatar.dart';
|
import '../common/contact_avatar.dart';
|
||||||
@@ -21,6 +23,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
final double Function(double, double, double, double)? calculateDistance;
|
final double Function(double, double, double, double)? calculateDistance;
|
||||||
final String Function(double)? formatDistance;
|
final String Function(double)? formatDistance;
|
||||||
final VoidCallback? onNavigateToMap;
|
final VoidCallback? onNavigateToMap;
|
||||||
|
final VoidCallback? onNavigateToMessages;
|
||||||
final int messageCount;
|
final int messageCount;
|
||||||
final int unreadMessageCount;
|
final int unreadMessageCount;
|
||||||
|
|
||||||
@@ -31,6 +34,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
this.calculateDistance,
|
this.calculateDistance,
|
||||||
this.formatDistance,
|
this.formatDistance,
|
||||||
this.onNavigateToMap,
|
this.onNavigateToMap,
|
||||||
|
this.onNavigateToMessages,
|
||||||
this.messageCount = 0,
|
this.messageCount = 0,
|
||||||
this.unreadMessageCount = 0,
|
this.unreadMessageCount = 0,
|
||||||
});
|
});
|
||||||
@@ -97,13 +101,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
void handleTap() {
|
void handleTap() => _handlePrimaryTap(context, contact);
|
||||||
if (contact.type == ContactType.chat) {
|
|
||||||
_showSetRouteDialog(context, contact);
|
|
||||||
} else {
|
|
||||||
_showContactDetails(context, contact);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final onLongPress = isPingInProgress
|
final onLongPress = isPingInProgress
|
||||||
? null
|
? null
|
||||||
@@ -305,6 +303,85 @@ class ContactTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handlePrimaryTap(BuildContext context, Contact contact) {
|
||||||
|
if (contact.isChannel) {
|
||||||
|
_showContactDetails(context, contact);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showContactActionSheet(context, contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showContactActionSheet(BuildContext context, Contact contact) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final canMessage =
|
||||||
|
contact.type == ContactType.chat || contact.type == ContactType.room;
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (sheetContext) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.message_outlined),
|
||||||
|
title: Text(l10n.messages),
|
||||||
|
enabled: canMessage,
|
||||||
|
onTap: !canMessage
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
await _openMessagesForContact(context, contact);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.alt_route),
|
||||||
|
title: const Text('Set path'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
_showSetRouteDialog(context, contact);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.delete, color: Colors.red),
|
||||||
|
title: Text(
|
||||||
|
l10n.deleteContact,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
_showDeleteConfirmation(context, contact);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openMessagesForContact(
|
||||||
|
BuildContext context,
|
||||||
|
Contact contact,
|
||||||
|
) async {
|
||||||
|
final messagesProvider = context.read<MessagesProvider>();
|
||||||
|
final destinationType = contact.type == ContactType.room
|
||||||
|
? MessageDestinationPreferences.destinationTypeRoom
|
||||||
|
: MessageDestinationPreferences.destinationTypeContact;
|
||||||
|
|
||||||
|
await MessageDestinationPreferences.setDestination(
|
||||||
|
destinationType,
|
||||||
|
recipientPublicKey: contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
messagesProvider.navigateToDestination(
|
||||||
|
destinationType,
|
||||||
|
recipientPublicKeyHex: contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
onNavigateToMessages?.call();
|
||||||
|
}
|
||||||
|
|
||||||
void _showRoomLoginDialog(BuildContext context, Contact contact) {
|
void _showRoomLoginDialog(BuildContext context, Contact contact) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
|||||||
|
|
||||||
bool _isSelected(String type, Contact? contact) {
|
bool _isSelected(String type, Contact? contact) {
|
||||||
if (widget.currentDestinationType != type) return false;
|
if (widget.currentDestinationType != type) return false;
|
||||||
if (contact == null) return false;
|
if (contact == null) return widget.currentRecipientPublicKey == null;
|
||||||
return contact.publicKeyHex == widget.currentRecipientPublicKey;
|
return contact.publicKeyHex == widget.currentRecipientPublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +138,18 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
|
_buildOptionTile(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.all_inbox,
|
||||||
|
title: l10n.showAll,
|
||||||
|
subtitle: 'All messages',
|
||||||
|
isSelected: _isSelected('all', null),
|
||||||
|
onTap: () {
|
||||||
|
widget.onSelect('all', null);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
// Channels section
|
// Channels section
|
||||||
if (widget.channels.isNotEmpty) ...[
|
if (widget.channels.isNotEmpty) ...[
|
||||||
Padding(
|
Padding(
|
||||||
@@ -348,4 +360,48 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
|||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildOptionTile({
|
||||||
|
required BuildContext context,
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required String subtitle,
|
||||||
|
required bool isSelected,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
radius: 20,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
child: Icon(icon, color: Theme.of(context).colorScheme.primary),
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
subtitle,
|
||||||
|
style: const TextStyle(fontSize: 12, fontFamily: 'monospace').copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: isSelected
|
||||||
|
? Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user