diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index b87c243..0138fa6 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -55,6 +55,8 @@ class CwtchNotifier { contact.isInvitation = data["authorization"] == "unknown"; contact.isBlocked = data["authorization"] == "blocked"; } + // contact.[status/isBlocked] might change the list's sort order + profileCN.getProfile(data["ProfileOnion"]).contactList.resort(); } break; case "NewMessageFromPeer": @@ -165,6 +167,7 @@ class CwtchNotifier { contact.status = data["ConnectionState"]; } }); + profileCN.getProfile(data["ProfileOnion"]).contactList.resort(); break; default: diff --git a/lib/model.dart b/lib/model.dart index 33dcf5b..684967f 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -80,7 +80,20 @@ class ProfileListState extends ChangeNotifier { class ContactListState extends ChangeNotifier { List _contacts = []; + String _filter; int get num => _contacts.length; + int get numFiltered => isFiltered ? filteredList().length : num; + bool get isFiltered => _filter != null && _filter != ""; + String get filter => _filter; + set filter(String newVal) { + _filter = newVal; + notifyListeners(); + } + + List filteredList() { + if (!isFiltered) return contacts; + return _contacts.where((ContactInfoState c) => c.onion.contains(_filter) || (c.nickname != null && c.nickname.contains(_filter))).toList(); + } void addAll(Iterable newContacts) { _contacts.addAll(newContacts); @@ -92,15 +105,24 @@ class ContactListState extends ChangeNotifier { notifyListeners(); } - void updateLastMessageTime(String forOnion, DateTime newVal) { - var contact = getContact(forOnion); - if (contact == null) return; - - contact.lastMessageTime = newVal; + void resort() { _contacts.sort((ContactInfoState a, ContactInfoState b) { - if (a.lastMessageTime == null && b.lastMessageTime == null) return b.onion.compareTo(a.onion); - if (a.lastMessageTime == null) return 1; - if (b.lastMessageTime == null) return -1; + // return -1 = a first in list + // return 1 = b first in list + // blocked contacts last + if (a.isBlocked == true && b.isBlocked != true) return 1; + if (a.isBlocked != true && b.isBlocked == true) return -1; + // special sorting for contacts with no messages in either history + if (a.lastMessageTime.millisecondsSinceEpoch == 0 && b.lastMessageTime.millisecondsSinceEpoch == 0) { + // online contacts first + if (a.isOnline() && !b.isOnline()) return -1; + if (!a.isOnline() && b.isOnline()) return 1; + // finally resort to onion + return a.onion.toString().compareTo(b.onion.toString()); + } + // finally... most recent history first + if (a.lastMessageTime.millisecondsSinceEpoch == 0) return 1; + if (b.lastMessageTime.millisecondsSinceEpoch == 0) return -1; return b.lastMessageTime.compareTo(a.lastMessageTime); }); // if(changed) { @@ -108,6 +130,14 @@ class ContactListState extends ChangeNotifier { //} } + void updateLastMessageTime(String forOnion, DateTime newVal) { + var contact = getContact(forOnion); + if (contact == null) return; + + contact.lastMessageTime = newVal; + resort(); + } + List get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier ContactInfoState getContact(String onion) { diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 9326d82..fc0138b 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_app/widgets/contactrow.dart'; import 'package:flutter_app/widgets/profileimage.dart'; +import 'package:flutter_app/widgets/textfield.dart'; import 'package:provider/provider.dart'; import '../settings.dart'; import 'addcontactview.dart'; @@ -15,6 +16,15 @@ class ContactsView extends StatefulWidget { } class _ContactsViewState extends State { + TextEditingController ctrlrFilter; + bool showSearchBar = false; + + @override + void initState() { + super.initState(); + ctrlrFilter = new TextEditingController(text: Provider.of(context, listen: false).filter); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -42,7 +52,16 @@ class _ContactsViewState extends State { IconButton( icon: Icon(Icons.bug_report), onPressed: _debugFakeMessage, - ) + ), + IconButton( + // need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset + icon: Icon(showSearchBar || Provider.of(context).isFiltered ? Icons.search_off : Icons.search), + onPressed: () { + Provider.of(context, listen: false).filter = ""; + setState(() { + showSearchBar = !showSearchBar; + }); + }) ], ), floatingActionButton: FloatingActionButton( @@ -50,13 +69,24 @@ class _ContactsViewState extends State { tooltip: AppLocalizations.of(context).tooltipAddContact, child: const Icon(Icons.person_add_sharp), ), - body: _buildContactList(), + body: showSearchBar || Provider.of(context).isFiltered ? _buildFilterable() : _buildContactList(), ); } + Widget _buildFilterable() { + //todo: translate + Widget txtfield = CwtchTextField( + controller: ctrlrFilter, + labelText: AppLocalizations.of(context).search, + onChanged: (newVal) { + Provider.of(context, listen: false).filter = newVal; + }); + return Column(children: [Padding(padding: EdgeInsets.all(2), child: txtfield), Expanded(child: _buildContactList())]); + } + Widget _buildContactList() { - final tiles = Provider.of(context).contacts.map((ContactInfoState contact) { - return ChangeNotifierProvider.value(value: contact, child: ContactRow()); + final tiles = Provider.of(context).filteredList().map((ContactInfoState contact) { + return ChangeNotifierProvider.value(key: ValueKey(contact.onion), value: contact, builder: (_, __) => ContactRow()); }); final divided = ListTile.divideTiles( context: context,