Merge pull request 'add contact sorting and filtering' (#90) from contactsortfilter into trunk
continuous-integration/drone/push Build is passing Details

Reviewed-on: #90
This commit is contained in:
Sarah Jamie Lewis 2021-05-18 18:24:05 -07:00
commit 1259574c55
3 changed files with 75 additions and 12 deletions

View File

@ -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:

View File

@ -80,7 +80,20 @@ class ProfileListState extends ChangeNotifier {
class ContactListState extends ChangeNotifier {
List<ContactInfoState> _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<ContactInfoState> 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<ContactInfoState> 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);
});
//<todo> if(changed) {
@ -108,6 +130,14 @@ class ContactListState extends ChangeNotifier {
//} </todo>
}
void updateLastMessageTime(String forOnion, DateTime newVal) {
var contact = getContact(forOnion);
if (contact == null) return;
contact.lastMessageTime = newVal;
resort();
}
List<ContactInfoState> get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
ContactInfoState getContact(String onion) {

View File

@ -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<ContactsView> {
TextEditingController ctrlrFilter;
bool showSearchBar = false;
@override
void initState() {
super.initState();
ctrlrFilter = new TextEditingController(text: Provider.of<ContactListState>(context, listen: false).filter);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -42,7 +52,16 @@ class _ContactsViewState extends State<ContactsView> {
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<ContactListState>(context).isFiltered ? Icons.search_off : Icons.search),
onPressed: () {
Provider.of<ContactListState>(context, listen: false).filter = "";
setState(() {
showSearchBar = !showSearchBar;
});
})
],
),
floatingActionButton: FloatingActionButton(
@ -50,13 +69,24 @@ class _ContactsViewState extends State<ContactsView> {
tooltip: AppLocalizations.of(context).tooltipAddContact,
child: const Icon(Icons.person_add_sharp),
),
body: _buildContactList(),
body: showSearchBar || Provider.of<ContactListState>(context).isFiltered ? _buildFilterable() : _buildContactList(),
);
}
Widget _buildFilterable() {
//todo: translate
Widget txtfield = CwtchTextField(
controller: ctrlrFilter,
labelText: AppLocalizations.of(context).search,
onChanged: (newVal) {
Provider.of<ContactListState>(context, listen: false).filter = newVal;
});
return Column(children: [Padding(padding: EdgeInsets.all(2), child: txtfield), Expanded(child: _buildContactList())]);
}
Widget _buildContactList() {
final tiles = Provider.of<ContactListState>(context).contacts.map((ContactInfoState contact) {
return ChangeNotifierProvider<ContactInfoState>.value(value: contact, child: ContactRow());
final tiles = Provider.of<ContactListState>(context).filteredList().map((ContactInfoState contact) {
return ChangeNotifierProvider<ContactInfoState>.value(key: ValueKey(contact.onion), value: contact, builder: (_, __) => ContactRow());
});
final divided = ListTile.divideTiles(
context: context,