import 'dart:async'; import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/apis/friend_api.dart'; import 'package:contacts_plus_plus/apis/message_api.dart'; import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; import 'package:contacts_plus_plus/widgets/expanding_input_fab.dart'; import 'package:contacts_plus_plus/widgets/friend_list_tile.dart'; import 'package:contacts_plus_plus/widgets/settings_page.dart'; import 'package:contacts_plus_plus/widgets/user_search.dart'; import 'package:flutter/material.dart'; class MenuItemDefinition { final String name; final IconData icon; final Function() onTap; const MenuItemDefinition({required this.name, required this.icon, required this.onTap}); } class FriendsList extends StatefulWidget { const FriendsList({super.key}); @override State createState() => _FriendsListState(); } class _FriendsListState extends State { static const Duration _autoRefreshDuration = Duration(seconds: 90); static const Duration _refreshTimeoutDuration = Duration(seconds: 30); Future>? _friendsFuture; ClientHolder? _clientHolder; Timer? _autoRefresh; Timer? _refreshTimeout; String _searchFilter = ""; @override void dispose() { _autoRefresh?.cancel(); _refreshTimeout?.cancel(); super.dispose(); } @override void didChangeDependencies() async { super.didChangeDependencies(); final clientHolder = ClientHolder.of(context); if (_clientHolder != clientHolder) { _clientHolder = clientHolder; final mClient = _clientHolder!.messagingClient; mClient.registerUnreadListener(() { if (context.mounted) { setState(() {}); } else { mClient.unregisterUnreadListener(); } }); _refreshFriendsList(); } } void _refreshFriendsList() { if (_refreshTimeout?.isActive == true) return; _friendsFuture = FriendApi.getFriendsList(_clientHolder!.apiClient).then((Iterable value) async { final unreadMessages = await MessageApi.getUserMessages(_clientHolder!.apiClient, unreadOnly: true); final mClient = _clientHolder?.messagingClient; if (mClient == null) return []; mClient.updateAllUnreads(unreadMessages.toList()); final friends = value.toList() ..sort((a, b) { var aVal = mClient.friendHasUnreads(a) ? -3 : 0; var bVal = mClient.friendHasUnreads(b) ? -3 : 0; aVal -= a.userStatus.lastStatusChange.compareTo(b.userStatus.lastStatusChange); aVal += a.userStatus.onlineStatus.compareTo(b.userStatus.onlineStatus) * 2; return aVal.compareTo(bVal); }); _autoRefresh?.cancel(); _autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList())); _refreshTimeout?.cancel(); _refreshTimeout = Timer(_refreshTimeoutDuration, () {}); _clientHolder?.messagingClient.updateFriendsCache(friends); return friends; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Contacts++"), actions: [ Padding( padding: const EdgeInsets.only(right: 4), child: PopupMenuButton( icon: const Icon(Icons.more_vert), onSelected: (MenuItemDefinition itemDef) async { await itemDef.onTap(); }, itemBuilder: (BuildContext context) => [ MenuItemDefinition(name: "Settings", icon: Icons.settings, onTap: () async { _autoRefresh?.cancel(); await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage())); _autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList())); }), MenuItemDefinition(name: "Find Users", icon: Icons.person_add, onTap: () async { bool changed = false; _autoRefresh?.cancel(); await Navigator.of(context).push( MaterialPageRoute( builder: (context) => UserSearch( onFriendsChanged: () => changed = true, ), ), ); if (changed) { _refreshTimeout?.cancel(); setState(() { _refreshFriendsList(); }); } else { _autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList())); } }), ].map((item) => PopupMenuItem( value: item, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(item.name), Icon(item.icon), ], ), ), ).toList(), ), ) ], ), body: Stack( children: [ RefreshIndicator( onRefresh: () async { _refreshFriendsList(); await _friendsFuture; // Keep the indicator running until everything's loaded }, child: FutureBuilder( future: _friendsFuture, builder: (context, snapshot) { if (snapshot.hasData) { var friends = (snapshot.data as List); if (_searchFilter.isNotEmpty) { friends = friends.where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase())).toList(); friends.sort((a, b) => a.username.length.compareTo(b.username.length)); } return ListView.builder( itemCount: friends.length, itemBuilder: (context, index) { final friend = friends[index]; final unreads = _clientHolder?.messagingClient.getUnreadsForFriend(friend) ?? []; return FriendListTile( friend: friend, unreads: unreads.length, onTap: () async { if (unreads.isNotEmpty) { final readBatch = MarkReadBatch( senderId: _clientHolder!.apiClient.userId, ids: unreads.map((e) => e.id).toList(), readTime: DateTime.now(), ); _clientHolder!.messagingClient.markMessagesRead(readBatch); } setState(() { unreads.clear(); }); }, ); }, ); } else if (snapshot.hasError) { FlutterError.reportError( FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace)); return DefaultErrorWidget( message: "${snapshot.error}", onRetry: () { _refreshTimeout?.cancel(); setState(() { _refreshFriendsList(); }); }, ); } else { return const LinearProgressIndicator(); } } ), ), Align( alignment: Alignment.bottomCenter, child: ExpandingInputFab( onInputChanged: (String text) { setState(() { _searchFilter = text; }); }, onExpansionChanged: (expanded) { if (!expanded) { setState(() { _searchFilter = ""; }); } }, ), ), ], ), ); } }