OpenContacts/lib/widgets/friends/friends_list.dart

335 lines
13 KiB
Dart
Raw Normal View History

2023-04-30 09:43:59 -04:00
import 'dart:async';
2023-05-05 09:05:06 -04:00
import 'package:contacts_plus_plus/apis/user_api.dart';
2023-05-05 06:45:00 -04:00
import 'package:contacts_plus_plus/client_holder.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart';
2023-05-01 13:13:40 -04:00
import 'package:contacts_plus_plus/models/friend.dart';
2023-05-05 09:05:06 -04:00
import 'package:contacts_plus_plus/models/personal_profile.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/friends/expanding_input_fab.dart';
import 'package:contacts_plus_plus/widgets/friends/friend_list_tile.dart';
2023-05-05 09:05:06 -04:00
import 'package:contacts_plus_plus/widgets/my_profile_dialog.dart';
import 'package:contacts_plus_plus/widgets/settings_page.dart';
import 'package:contacts_plus_plus/widgets/friends/user_search.dart';
2023-04-29 13:18:46 -04:00
import 'package:flutter/material.dart';
2023-05-05 10:39:40 -04:00
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
2023-04-29 13:18:46 -04:00
2023-05-04 12:16:19 -04:00
class MenuItemDefinition {
final String name;
final IconData icon;
2023-05-04 13:38:35 -04:00
final Function() onTap;
2023-05-04 12:16:19 -04:00
const MenuItemDefinition({required this.name, required this.icon, required this.onTap});
}
class FriendsList extends StatefulWidget {
const FriendsList({super.key});
2023-04-29 13:18:46 -04:00
@override
State<FriendsList> createState() => _FriendsListState();
2023-04-29 13:18:46 -04:00
}
class _FriendsListState extends State<FriendsList> with AutomaticKeepAliveClientMixin {
2023-05-03 12:43:06 -04:00
String _searchFilter = "";
2023-04-30 09:43:59 -04:00
2023-04-29 13:18:46 -04:00
@override
Widget build(BuildContext context) {
super.build(context);
return Stack(
alignment: Alignment.topCenter,
2023-04-30 09:43:59 -04:00
children: [
Consumer<MessagingClient>(
builder: (context, mClient, _) {
if (mClient.initStatus == null) {
return const LinearProgressIndicator();
} else if (mClient.initStatus!.isNotEmpty) {
return Column(
children: [
Expanded(
child: DefaultErrorWidget(
message: mClient.initStatus,
onRetry: () async {
mClient.resetStatus();
mClient.refreshFriendsListWithErrorHandler();
},
),
),
],
);
} else {
var friends = List.from(mClient.cachedFriends); // Explicit copy.
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));
2023-04-30 09:43:59 -04:00
}
return ListView.builder(
itemCount: friends.length,
itemBuilder: (context, index) {
final friend = friends[index];
final unreads = mClient.getUnreadsForFriend(friend);
return FriendListTile(
friend: friend,
unreads: unreads.length,
);
},
);
2023-04-30 09:43:59 -04:00
}
}
2023-04-30 09:43:59 -04:00
),
Align(
alignment: Alignment.bottomCenter,
child: ExpandingInputFab(
onInputChanged: (String text) {
2023-04-30 17:14:29 -04:00
setState(() {
2023-05-03 12:43:06 -04:00
_searchFilter = text;
2023-04-30 09:43:59 -04:00
});
},
onExpansionChanged: (expanded) {
if (!expanded) {
setState(() {
2023-05-03 12:43:06 -04:00
_searchFilter = "";
2023-04-30 09:43:59 -04:00
});
}
},
),
),
],
);
}
@override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
}
class FriendsListAppBar extends StatefulWidget implements PreferredSizeWidget {
const FriendsListAppBar({required this.mClient, super.key});
// Passing this instance around like this is kinda dirty, I want to try to find a cleaner way to do this using Provider
final MessagingClient mClient;
@override
State<StatefulWidget> createState() => _FriendsListAppBarState();
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _FriendsListAppBarState extends State<FriendsListAppBar> {
Future<UserStatus>? _userStatusFuture;
Future<PersonalProfile>? _userProfileFuture;
ClientHolder? _clientHolder;
@override
void didChangeDependencies() async {
super.didChangeDependencies();
final clientHolder = ClientHolder.of(context);
if (_clientHolder != clientHolder) {
_clientHolder = clientHolder;
final apiClient = _clientHolder!.apiClient;
_userProfileFuture = UserApi.getPersonalProfile(apiClient);
_refreshUserStatus();
}
}
void _refreshUserStatus() {
final apiClient = _clientHolder!.apiClient;
_userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async {
if (value.onlineStatus == OnlineStatus.offline) {
final newStatus = value.copyWith(
onlineStatus: OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus
.valueOrDefault]
);
await UserApi.setStatus(apiClient, status: newStatus);
return newStatus;
}
return value;
});
}
@override
Widget build(BuildContext context) {
final clientHolder = ClientHolder.of(context);
return AppBar(
title: const Text("Contacts++"),
actions: [
FutureBuilder(
future: _userStatusFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
final userStatus = snapshot.data as UserStatus;
return PopupMenuButton<OnlineStatus>(
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Icon(Icons.circle, size: 16, color: userStatus.onlineStatus.color,),
),
Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"),
],
),
onSelected: (OnlineStatus onlineStatus) async {
try {
final newStatus = userStatus.copyWith(onlineStatus: onlineStatus);
setState(() {
_userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now()));
});
final settingsClient = clientHolder.settingsClient;
await UserApi.setStatus(clientHolder.apiClient, status: newStatus);
await settingsClient.changeSettings(
settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index));
} catch (e, s) {
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text(
"Failed to set online-status.")));
setState(() {
_userStatusFuture = Future.value(userStatus);
});
}
},
itemBuilder: (BuildContext context) =>
OnlineStatus.values.where((element) =>
element == OnlineStatus.online
|| element == OnlineStatus.invisible).map((item) =>
PopupMenuItem<OnlineStatus>(
value: item,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(Icons.circle, size: 16, color: item.color,),
const SizedBox(width: 8,),
Text(toBeginningOfSentenceCase(item.name)!),
],
),
),
).toList());
} else if (snapshot.hasError) {
return TextButton.icon(
style: TextButton.styleFrom(
foregroundColor: Theme
.of(context)
.colorScheme
.onSurface,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2)
),
onPressed: () {
setState(() {
_userStatusFuture = null;
});
setState(() {
_userStatusFuture = UserApi.getUserStatus(clientHolder.apiClient, userId: clientHolder.apiClient
.userId);
});
},
icon: const Icon(Icons.warning),
label: const Text("Retry"),
);
} else {
return TextButton.icon(
style: TextButton.styleFrom(
disabledForegroundColor: Theme
.of(context)
.colorScheme
.onSurface,
),
onPressed: null,
icon: Container(
width: 16,
height: 16,
margin: const EdgeInsets.only(right: 4),
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme
.of(context)
.colorScheme
.onSurface,
),
),
label: const Text("Loading"),
);
}
}
),
Padding(
padding: const EdgeInsets.only(left: 4, right: 4),
child: PopupMenuButton<MenuItemDefinition>(
icon: const Icon(Icons.more_vert),
onSelected: (MenuItemDefinition itemDef) async {
await itemDef.onTap();
},
itemBuilder: (BuildContext context) =>
[
MenuItemDefinition(
name: "Settings",
icon: Icons.settings,
onTap: () async {
await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage()));
},
),
MenuItemDefinition(
name: "Find Users",
icon: Icons.person_add,
onTap: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
ChangeNotifierProvider<MessagingClient>.value(
value: widget.mClient,
child: const UserSearch(),
),
),
);
},
),
MenuItemDefinition(
name: "My Profile",
icon: Icons.person,
onTap: () async {
await showDialog(
context: context,
builder: (context) {
return FutureBuilder(
future: _userProfileFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
final profile = snapshot.data as PersonalProfile;
return MyProfileDialog(profile: profile);
} else if (snapshot.hasError) {
return DefaultErrorWidget(
title: "Failed to load personal profile.",
onRetry: () {
setState(() {
_userProfileFuture = UserApi.getPersonalProfile(clientHolder.apiClient);
});
},
);
} else {
return const Center(child: CircularProgressIndicator(),);
}
}
);
},
);
},
),
].map((item) =>
PopupMenuItem<MenuItemDefinition>(
value: item,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(item.name),
Icon(item.icon),
],
),
),
).toList(),
),
)
],
2023-04-29 13:18:46 -04:00
);
}
}