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';
|
2023-05-06 10:45:26 -04:00
|
|
|
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';
|
2023-05-03 15:55:34 -04:00
|
|
|
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
|
2023-05-06 13:24:28 -04:00
|
|
|
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';
|
2023-05-03 11:51:18 -04:00
|
|
|
import 'package:contacts_plus_plus/widgets/settings_page.dart';
|
2023-05-06 13:24:28 -04:00
|
|
|
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';
|
2023-05-06 10:45:26 -04:00
|
|
|
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});
|
|
|
|
}
|
|
|
|
|
2023-05-03 11:51:18 -04:00
|
|
|
class FriendsList extends StatefulWidget {
|
|
|
|
const FriendsList({super.key});
|
2023-04-29 13:18:46 -04:00
|
|
|
|
|
|
|
@override
|
2023-05-03 11:51:18 -04:00
|
|
|
State<FriendsList> createState() => _FriendsListState();
|
2023-04-29 13:18:46 -04:00
|
|
|
}
|
|
|
|
|
2023-05-03 11:51:18 -04:00
|
|
|
class _FriendsListState extends State<FriendsList> {
|
2023-05-05 09:05:06 -04:00
|
|
|
Future<PersonalProfile>? _userProfileFuture;
|
2023-05-05 10:39:40 -04:00
|
|
|
Future<UserStatus>? _userStatusFuture;
|
2023-04-30 07:39:09 -04:00
|
|
|
ClientHolder? _clientHolder;
|
2023-05-03 12:43:06 -04:00
|
|
|
String _searchFilter = "";
|
2023-04-30 09:43:59 -04:00
|
|
|
|
2023-04-29 15:26:12 -04:00
|
|
|
@override
|
2023-05-03 17:24:27 -04:00
|
|
|
void didChangeDependencies() async {
|
2023-04-30 07:39:09 -04:00
|
|
|
super.didChangeDependencies();
|
|
|
|
final clientHolder = ClientHolder.of(context);
|
|
|
|
if (_clientHolder != clientHolder) {
|
|
|
|
_clientHolder = clientHolder;
|
2023-05-05 10:39:40 -04:00
|
|
|
final apiClient = _clientHolder!.apiClient;
|
|
|
|
_userProfileFuture = UserApi.getPersonalProfile(apiClient);
|
2023-05-06 10:45:26 -04:00
|
|
|
_refreshUserStatus();
|
2023-04-30 07:39:09 -04:00
|
|
|
}
|
2023-04-29 15:26:12 -04:00
|
|
|
}
|
|
|
|
|
2023-05-06 10:45:26 -04:00
|
|
|
void _refreshUserStatus() {
|
2023-05-05 10:39:40 -04:00
|
|
|
final apiClient = _clientHolder!.apiClient;
|
|
|
|
_userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async {
|
|
|
|
if (value.onlineStatus == OnlineStatus.offline) {
|
|
|
|
final newStatus = value.copyWith(
|
2023-05-06 10:45:26 -04:00
|
|
|
onlineStatus: OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus
|
|
|
|
.valueOrDefault]
|
2023-05-05 10:39:40 -04:00
|
|
|
);
|
|
|
|
await UserApi.setStatus(apiClient, status: newStatus);
|
|
|
|
return newStatus;
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
});
|
2023-04-29 15:26:12 -04:00
|
|
|
}
|
2023-04-29 13:18:46 -04:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2023-05-06 10:45:26 -04:00
|
|
|
final clientHolder = ClientHolder.of(context);
|
2023-04-29 13:18:46 -04:00
|
|
|
return Scaffold(
|
|
|
|
appBar: AppBar(
|
2023-04-30 09:43:59 -04:00
|
|
|
title: const Text("Contacts++"),
|
2023-05-03 11:51:18 -04:00
|
|
|
actions: [
|
2023-05-05 10:39:40 -04:00
|
|
|
FutureBuilder(
|
2023-05-06 10:45:26 -04:00
|
|
|
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
|
|
|
|
.of(context)
|
|
|
|
.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) =>
|
2023-05-06 13:01:15 -04:00
|
|
|
OnlineStatus.values.where((element) =>
|
|
|
|
element == OnlineStatus.online
|
|
|
|
|| element == OnlineStatus.invisible).map((item) =>
|
2023-05-06 10:45:26 -04:00
|
|
|
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)
|
2023-05-05 10:39:40 -04:00
|
|
|
),
|
2023-05-06 10:45:26 -04:00
|
|
|
onPressed: () {
|
|
|
|
setState(() {
|
|
|
|
_userStatusFuture = UserApi.getUserStatus(clientHolder.apiClient, userId: clientHolder.apiClient
|
|
|
|
.userId);
|
|
|
|
});
|
2023-05-16 07:01:42 -04:00
|
|
|
|
2023-05-05 10:39:40 -04:00
|
|
|
},
|
2023-05-06 10:45:26 -04:00
|
|
|
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,
|
|
|
|
),
|
2023-05-05 10:39:40 -04:00
|
|
|
),
|
2023-05-06 10:45:26 -04:00
|
|
|
label: const Text("Loading"),
|
|
|
|
);
|
|
|
|
}
|
2023-05-05 10:39:40 -04:00
|
|
|
}
|
|
|
|
),
|
2023-05-04 12:16:19 -04:00
|
|
|
Padding(
|
2023-05-05 10:39:40 -04:00
|
|
|
padding: const EdgeInsets.only(left: 4, right: 4),
|
2023-05-04 12:16:19 -04:00
|
|
|
child: PopupMenuButton<MenuItemDefinition>(
|
|
|
|
icon: const Icon(Icons.more_vert),
|
2023-05-04 13:38:35 -04:00
|
|
|
onSelected: (MenuItemDefinition itemDef) async {
|
|
|
|
await itemDef.onTap();
|
2023-05-04 12:16:19 -04:00
|
|
|
},
|
2023-05-05 09:05:06 -04:00
|
|
|
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 {
|
2023-05-06 10:45:26 -04:00
|
|
|
final mClient = Provider.of<MessagingClient>(context, listen: false);
|
2023-05-05 09:05:06 -04:00
|
|
|
await Navigator.of(context).push(
|
|
|
|
MaterialPageRoute(
|
|
|
|
builder: (context) =>
|
2023-05-06 13:01:15 -04:00
|
|
|
ChangeNotifierProvider<MessagingClient>.value(
|
|
|
|
value: mClient,
|
|
|
|
child: const UserSearch(),
|
2023-05-05 09:05:06 -04:00
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
MenuItemDefinition(
|
|
|
|
name: "My Profile",
|
|
|
|
icon: Icons.person,
|
|
|
|
onTap: () async {
|
|
|
|
await showDialog(
|
|
|
|
context: context,
|
|
|
|
builder: (context) {
|
|
|
|
return FutureBuilder(
|
2023-05-06 10:45:26 -04:00
|
|
|
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
|
|
|
|
.of(context)
|
|
|
|
.apiClient);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return const Center(child: CircularProgressIndicator(),);
|
|
|
|
}
|
2023-05-05 09:05:06 -04:00
|
|
|
}
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
2023-05-04 12:16:19 -04:00
|
|
|
].map((item) =>
|
|
|
|
PopupMenuItem<MenuItemDefinition>(
|
|
|
|
value: item,
|
|
|
|
child: Row(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
children: [
|
|
|
|
Text(item.name),
|
|
|
|
Icon(item.icon),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
).toList(),
|
|
|
|
),
|
2023-05-03 11:51:18 -04:00
|
|
|
)
|
|
|
|
],
|
2023-04-29 13:18:46 -04:00
|
|
|
),
|
2023-04-30 09:43:59 -04:00
|
|
|
body: Stack(
|
2023-05-06 10:57:08 -04:00
|
|
|
alignment: Alignment.topCenter,
|
2023-04-30 09:43:59 -04:00
|
|
|
children: [
|
2023-05-06 10:45:26 -04:00
|
|
|
Consumer<MessagingClient>(
|
|
|
|
builder: (context, mClient, _) {
|
2023-05-06 10:57:08 -04:00
|
|
|
if (mClient.initStatus == null) {
|
|
|
|
return const LinearProgressIndicator();
|
|
|
|
} else if (mClient.initStatus!.isNotEmpty) {
|
2023-05-06 10:45:26 -04:00
|
|
|
return Column(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
child: DefaultErrorWidget(
|
2023-05-06 10:57:08 -04:00
|
|
|
message: mClient.initStatus,
|
2023-05-06 10:45:26 -04:00
|
|
|
onRetry: () async {
|
2023-05-06 10:57:08 -04:00
|
|
|
mClient.resetStatus();
|
2023-05-06 10:45:26 -04:00
|
|
|
mClient.refreshFriendsListWithErrorHandler();
|
|
|
|
},
|
2023-05-06 10:57:08 -04:00
|
|
|
),
|
|
|
|
),
|
2023-05-06 10:45:26 -04:00
|
|
|
],
|
|
|
|
);
|
|
|
|
} 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
|
|
|
}
|
2023-05-06 10:45:26 -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-05-06 10:45:26 -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
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2023-04-29 13:18:46 -04:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2023-05-06 10:45:26 -04:00
|
|
|
}
|