Implement friend add and remove buttons

This commit is contained in:
Nutcake 2023-05-04 20:57:16 +02:00
parent 5b4f509840
commit 4e5ac6a8d4
9 changed files with 124 additions and 16 deletions

View file

@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/friend.dart';
import 'package:contacts_plus_plus/models/user.dart';
class FriendApi { class FriendApi {
static Future<Iterable<Friend>> getFriendsList(ApiClient client) async { static Future<Iterable<Friend>> getFriendsList(ApiClient client) async {
@ -12,9 +11,4 @@ class FriendApi {
final data = jsonDecode(response.body) as List; final data = jsonDecode(response.body) as List;
return data.map((e) => Friend.fromMap(e)); return data.map((e) => Friend.fromMap(e));
} }
static Future<void> addFriend(ApiClient client, {required User user}) async {
final response = await client.put("/users/${client.userId}/friends/${user.id}", body: user.toMap());
ApiClient.checkResponse(response);
}
} }

View file

@ -1,7 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/friend.dart';
import 'package:contacts_plus_plus/models/user.dart'; import 'package:contacts_plus_plus/models/user.dart';
import 'package:contacts_plus_plus/models/user_profile.dart';
class UserApi { class UserApi {
static Future<Iterable<User>> searchUsers(ApiClient client, {required String needle}) async { static Future<Iterable<User>> searchUsers(ApiClient client, {required String needle}) async {
@ -10,4 +12,23 @@ class UserApi {
final data = jsonDecode(response.body) as List; final data = jsonDecode(response.body) as List;
return data.map((e) => User.fromMap(e)); return data.map((e) => User.fromMap(e));
} }
static Future<void> addUserAsFriend(ApiClient client, {required User user}) async {
final friend = Friend(
id: user.id,
username: user.username,
ownerId: client.userId,
userStatus: UserStatus.empty(),
userProfile: UserProfile.empty(),
friendStatus: FriendStatus.accepted,
);
final body = jsonEncode(friend.toMap(shallow: true));
final response = await client.put("/users/${client.userId}/friends/${user.id}", body: body);
ApiClient.checkResponse(response);
}
static Future<void> removeUserAsFriend(ApiClient client, {required User user}) async {
final response = await client.delete("/users/${client.userId}/friends/${user.id}");
ApiClient.checkResponse(response);
}
} }

View file

@ -134,6 +134,7 @@ class ApiClient {
Future<http.Response> put(String path, {Object? body, Map<String, String>? headers}) { Future<http.Response> put(String path, {Object? body, Map<String, String>? headers}) {
headers ??= {}; headers ??= {};
headers["Content-Type"] = "application/json";
headers.addAll(authorizationHeader); headers.addAll(authorizationHeader);
return http.put(buildFullUri(path), headers: headers, body: body); return http.put(buildFullUri(path), headers: headers, body: body);
} }

View file

@ -6,11 +6,12 @@ import 'package:contacts_plus_plus/models/user_profile.dart';
class Friend extends Comparable { class Friend extends Comparable {
final String id; final String id;
final String username; final String username;
final String ownerId;
final UserStatus userStatus; final UserStatus userStatus;
final UserProfile userProfile; final UserProfile userProfile;
final FriendStatus friendStatus; final FriendStatus friendStatus;
Friend({required this.id, required this.username, required this.userStatus, required this.userProfile, Friend({required this.id, required this.username, required this.ownerId, required this.userStatus, required this.userProfile,
required this.friendStatus, required this.friendStatus,
}); });
@ -18,12 +19,24 @@ class Friend extends Comparable {
return Friend( return Friend(
id: map["id"], id: map["id"],
username: map["friendUsername"], username: map["friendUsername"],
ownerId: map["ownerId"] ?? map["id"],
userStatus: UserStatus.fromMap(map["userStatus"]), userStatus: UserStatus.fromMap(map["userStatus"]),
userProfile: UserProfile.fromMap(map["profile"] ?? {}), userProfile: UserProfile.fromMap(map["profile"] ?? {}),
friendStatus: FriendStatus.fromString(map["friendStatus"]), friendStatus: FriendStatus.fromString(map["friendStatus"]),
); );
} }
Map toMap({bool shallow=false}) {
return {
"id": id,
"username": username,
"ownerId": ownerId,
"userStatus": userStatus.toMap(shallow: shallow),
"profile": userProfile.toMap(),
"friendStatus": friendStatus.name,
};
}
@override @override
int compareTo(covariant Friend other) { int compareTo(covariant Friend other) {
return username.compareTo(other.username); return username.compareTo(other.username);
@ -75,9 +88,14 @@ class UserStatus {
final DateTime lastStatusChange; final DateTime lastStatusChange;
final List<Session> activeSessions; final List<Session> activeSessions;
UserStatus({required this.onlineStatus, required this.lastStatusChange, required this.activeSessions}); UserStatus({required this.onlineStatus, required this.lastStatusChange, required this.activeSessions});
factory UserStatus.empty() => UserStatus(
onlineStatus: OnlineStatus.unknown,
lastStatusChange: DateTime.now(),
activeSessions: [],
);
factory UserStatus.fromMap(Map map) { factory UserStatus.fromMap(Map map) {
final statusString = map["onlineStatus"] as String?; final statusString = map["onlineStatus"] as String?;
final status = OnlineStatus.fromString(statusString); final status = OnlineStatus.fromString(statusString);
@ -90,4 +108,12 @@ class UserStatus {
activeSessions: (map["activeSessions"] as List? ?? []).map((e) => Session.fromMap(e)).toList(), activeSessions: (map["activeSessions"] as List? ?? []).map((e) => Session.fromMap(e)).toList(),
); );
} }
Map toMap({bool shallow=false}) {
return {
"onlineStatus": onlineStatus.index,
"lastStatusChange": lastStatusChange.toIso8601String(),
"activeSessions": shallow ? [] : activeSessions.map((e) => e.toMap(),)
};
}
} }

View file

@ -32,6 +32,22 @@ class Session {
); );
} }
Map toMap({bool shallow=false}) {
return {
"sessionId": id,
"name": name,
"sessionUsers": shallow ? [] : throw UnimplementedError(),
"thumbnail": thumbnail,
"maxUsers": maxUsers,
"hasEnded": hasEnded,
"isValid": isValid,
"description": description,
"tags": shallow ? [] : throw UnimplementedError(),
"headlessHost": headlessHost,
"hostUsername": hostUsername,
};
}
bool get isLive => !hasEnded && isValid; bool get isLive => !hasEnded && isValid;
} }

View file

@ -3,6 +3,8 @@ class UserProfile {
UserProfile({required this.iconUrl}); UserProfile({required this.iconUrl});
factory UserProfile.empty() => UserProfile(iconUrl: "");
factory UserProfile.fromMap(Map map) { factory UserProfile.fromMap(Map map) {
return UserProfile(iconUrl: map["iconUrl"] ?? ""); return UserProfile(iconUrl: map["iconUrl"] ?? "");
} }

View file

@ -110,10 +110,24 @@ class _FriendsListState extends State<FriendsList> {
_autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList())); _autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList()));
}), }),
MenuItemDefinition(name: "Find Users", icon: Icons.person_add, onTap: () async { MenuItemDefinition(name: "Find Users", icon: Icons.person_add, onTap: () async {
bool changed = false;
_autoRefresh?.cancel(); _autoRefresh?.cancel();
await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const UserSearch())); await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
UserSearch(
onFriendsChanged: () => changed = true,
),
),
);
if (changed) {
_refreshTimeout?.cancel();
setState(() {
_refreshFriendsList();
});
} else {
_autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList())); _autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList()));
}
}) })
].map((item) => ].map((item) =>
PopupMenuItem<MenuItemDefinition>( PopupMenuItem<MenuItemDefinition>(
@ -144,7 +158,7 @@ class _FriendsListState extends State<FriendsList> {
if (snapshot.hasData) { if (snapshot.hasData) {
var friends = (snapshot.data as List<Friend>); var friends = (snapshot.data as List<Friend>);
if (_searchFilter.isNotEmpty) { if (_searchFilter.isNotEmpty) {
friends = friends.where((element) => element.username.contains(_searchFilter)).toList(); friends = friends.where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase())).toList();
friends.sort((a, b) => a.username.length.compareTo(b.username.length)); friends.sort((a, b) => a.username.length.compareTo(b.username.length));
} }
return ListView.builder( return ListView.builder(

View file

@ -1,14 +1,17 @@
import 'package:contacts_plus_plus/apis/user_api.dart';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/user.dart'; import 'package:contacts_plus_plus/models/user.dart';
import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class UserListTile extends StatefulWidget { class UserListTile extends StatefulWidget {
const UserListTile({required this.user, required this.isFriend, super.key}); const UserListTile({required this.user, required this.isFriend, required this.onChange, super.key});
final User user; final User user;
final bool isFriend; final bool isFriend;
final Function()? onChange;
@override @override
State<UserListTile> createState() => _UserListTileState(); State<UserListTile> createState() => _UserListTileState();
@ -17,6 +20,7 @@ class UserListTile extends StatefulWidget {
class _UserListTileState extends State<UserListTile> { class _UserListTileState extends State<UserListTile> {
final DateFormat _regDateFormat = DateFormat.yMMMMd('en_US'); final DateFormat _regDateFormat = DateFormat.yMMMMd('en_US');
late bool _localAdded = widget.isFriend; late bool _localAdded = widget.isFriend;
bool _loading = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -25,10 +29,38 @@ class _UserListTileState extends State<UserListTile> {
title: Text(widget.user.username), title: Text(widget.user.username),
subtitle: Text(_regDateFormat.format(widget.user.registrationDate)), subtitle: Text(_regDateFormat.format(widget.user.registrationDate)),
trailing: IconButton( trailing: IconButton(
onPressed: () { onPressed: _loading ? null : () async {
setState(() { setState(() {
_loading = true;
});
try {
if (_localAdded) {
await UserApi.removeUserAsFriend(ClientHolder.of(context).apiClient, user: widget.user);
} else {
await UserApi.addUserAsFriend(ClientHolder.of(context).apiClient, user: widget.user);
}
} catch (e, s) {
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 5),
content: Text(
"Something went wrong: $e",
softWrap: true,
maxLines: null,
),
),
);
setState(() {
_loading = false;
});
return;
}
setState(() {
_loading = false;
_localAdded = !_localAdded; _localAdded = !_localAdded;
}); });
widget.onChange?.call();
}, },
splashRadius: 24, splashRadius: 24,
icon: _localAdded ? const Icon(Icons.person_remove_alt_1) : const Icon(Icons.person_add_alt_1), icon: _localAdded ? const Icon(Icons.person_remove_alt_1) : const Icon(Icons.person_add_alt_1),

View file

@ -16,7 +16,9 @@ class SearchError {
} }
class UserSearch extends StatefulWidget { class UserSearch extends StatefulWidget {
const UserSearch({super.key}); const UserSearch({required this.onFriendsChanged, super.key});
final Function()? onFriendsChanged;
@override @override
State<StatefulWidget> createState() => _UserSearchState(); State<StatefulWidget> createState() => _UserSearchState();
@ -71,7 +73,7 @@ class _UserSearchState extends State<UserSearch> {
itemCount: users.length, itemCount: users.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final user = users[index]; final user = users[index];
return UserListTile(user: user, isFriend: mClient.getAsFriend(user.id) != null,); return UserListTile(user: user, isFriend: mClient.getAsFriend(user.id) != null, onChange: widget.onFriendsChanged);
}, },
); );
} else if (snapshot.hasError) { } else if (snapshot.hasError) {