Display unread counts per channel

This commit is contained in:
Janez T
2026-03-08 20:22:10 +01:00
parent bfd15065f0
commit 771cddb80d
7 changed files with 344 additions and 24 deletions

View File

@@ -8,6 +8,8 @@ import '../../models/room_login_state.dart';
import '../../providers/connection_provider.dart';
import '../../providers/contacts_provider.dart';
import '../../providers/map_provider.dart';
import '../../providers/messages_provider.dart';
import '../../services/message_destination_preferences.dart';
import 'contact_route_dialog.dart';
import 'room_login_sheet.dart';
import '../common/contact_avatar.dart';
@@ -21,6 +23,7 @@ class ContactTile extends StatelessWidget {
final double Function(double, double, double, double)? calculateDistance;
final String Function(double)? formatDistance;
final VoidCallback? onNavigateToMap;
final VoidCallback? onNavigateToMessages;
final int messageCount;
final int unreadMessageCount;
@@ -31,6 +34,7 @@ class ContactTile extends StatelessWidget {
this.calculateDistance,
this.formatDistance,
this.onNavigateToMap,
this.onNavigateToMessages,
this.messageCount = 0,
this.unreadMessageCount = 0,
});
@@ -97,13 +101,7 @@ class ContactTile extends StatelessWidget {
],
)
: null;
void handleTap() {
if (contact.type == ContactType.chat) {
_showSetRouteDialog(context, contact);
} else {
_showContactDetails(context, contact);
}
}
void handleTap() => _handlePrimaryTap(context, contact);
final onLongPress = isPingInProgress
? 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) {
showModalBottomSheet(
context: context,

View File

@@ -47,7 +47,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
bool _isSelected(String type, Contact? contact) {
if (widget.currentDestinationType != type) return false;
if (contact == null) return false;
if (contact == null) return widget.currentRecipientPublicKey == null;
return contact.publicKeyHex == widget.currentRecipientPublicKey;
}
@@ -138,6 +138,18 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
child: ListView(
shrinkWrap: true,
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
if (widget.channels.isNotEmpty) ...[
Padding(
@@ -348,4 +360,48 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
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,
);
}
}