Implement friend add and remove buttons
This commit is contained in:
parent
5b4f509840
commit
4e5ac6a8d4
9 changed files with 124 additions and 16 deletions
|
@ -3,7 +3,6 @@ import 'dart:convert';
|
|||
|
||||
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';
|
||||
|
||||
class FriendApi {
|
||||
static Future<Iterable<Friend>> getFriendsList(ApiClient client) async {
|
||||
|
@ -12,9 +11,4 @@ class FriendApi {
|
|||
final data = jsonDecode(response.body) as List;
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import 'dart:convert';
|
||||
|
||||
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_profile.dart';
|
||||
|
||||
class UserApi {
|
||||
static Future<Iterable<User>> searchUsers(ApiClient client, {required String needle}) async {
|
||||
|
@ -10,4 +12,23 @@ class UserApi {
|
|||
final data = jsonDecode(response.body) as List;
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -134,6 +134,7 @@ class ApiClient {
|
|||
|
||||
Future<http.Response> put(String path, {Object? body, Map<String, String>? headers}) {
|
||||
headers ??= {};
|
||||
headers["Content-Type"] = "application/json";
|
||||
headers.addAll(authorizationHeader);
|
||||
return http.put(buildFullUri(path), headers: headers, body: body);
|
||||
}
|
||||
|
|
|
@ -6,11 +6,12 @@ import 'package:contacts_plus_plus/models/user_profile.dart';
|
|||
class Friend extends Comparable {
|
||||
final String id;
|
||||
final String username;
|
||||
final String ownerId;
|
||||
final UserStatus userStatus;
|
||||
final UserProfile userProfile;
|
||||
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,
|
||||
});
|
||||
|
||||
|
@ -18,12 +19,24 @@ class Friend extends Comparable {
|
|||
return Friend(
|
||||
id: map["id"],
|
||||
username: map["friendUsername"],
|
||||
ownerId: map["ownerId"] ?? map["id"],
|
||||
userStatus: UserStatus.fromMap(map["userStatus"]),
|
||||
userProfile: UserProfile.fromMap(map["profile"] ?? {}),
|
||||
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
|
||||
int compareTo(covariant Friend other) {
|
||||
return username.compareTo(other.username);
|
||||
|
@ -75,9 +88,14 @@ class UserStatus {
|
|||
final DateTime lastStatusChange;
|
||||
final List<Session> 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) {
|
||||
final statusString = map["onlineStatus"] as String?;
|
||||
final status = OnlineStatus.fromString(statusString);
|
||||
|
@ -90,4 +108,12 @@ class UserStatus {
|
|||
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(),)
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ class UserProfile {
|
|||
|
||||
UserProfile({required this.iconUrl});
|
||||
|
||||
factory UserProfile.empty() => UserProfile(iconUrl: "");
|
||||
|
||||
factory UserProfile.fromMap(Map map) {
|
||||
return UserProfile(iconUrl: map["iconUrl"] ?? "");
|
||||
}
|
||||
|
|
|
@ -110,10 +110,24 @@ class _FriendsListState extends State<FriendsList> {
|
|||
_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) => const UserSearch()));
|
||||
_autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList()));
|
||||
|
||||
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<MenuItemDefinition>(
|
||||
|
@ -144,7 +158,7 @@ class _FriendsListState extends State<FriendsList> {
|
|||
if (snapshot.hasData) {
|
||||
var friends = (snapshot.data as List<Friend>);
|
||||
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));
|
||||
}
|
||||
return ListView.builder(
|
||||
|
|
|
@ -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/clients/api_client.dart';
|
||||
import 'package:contacts_plus_plus/models/user.dart';
|
||||
import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
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 bool isFriend;
|
||||
final Function()? onChange;
|
||||
|
||||
@override
|
||||
State<UserListTile> createState() => _UserListTileState();
|
||||
|
@ -17,6 +20,7 @@ class UserListTile extends StatefulWidget {
|
|||
class _UserListTileState extends State<UserListTile> {
|
||||
final DateFormat _regDateFormat = DateFormat.yMMMMd('en_US');
|
||||
late bool _localAdded = widget.isFriend;
|
||||
bool _loading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -25,10 +29,38 @@ class _UserListTileState extends State<UserListTile> {
|
|||
title: Text(widget.user.username),
|
||||
subtitle: Text(_regDateFormat.format(widget.user.registrationDate)),
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
onPressed: _loading ? null : () async {
|
||||
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;
|
||||
});
|
||||
widget.onChange?.call();
|
||||
},
|
||||
splashRadius: 24,
|
||||
icon: _localAdded ? const Icon(Icons.person_remove_alt_1) : const Icon(Icons.person_add_alt_1),
|
||||
|
|
|
@ -16,7 +16,9 @@ class SearchError {
|
|||
}
|
||||
|
||||
class UserSearch extends StatefulWidget {
|
||||
const UserSearch({super.key});
|
||||
const UserSearch({required this.onFriendsChanged, super.key});
|
||||
|
||||
final Function()? onFriendsChanged;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _UserSearchState();
|
||||
|
@ -71,7 +73,7 @@ class _UserSearchState extends State<UserSearch> {
|
|||
itemCount: users.length,
|
||||
itemBuilder: (context, 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) {
|
||||
|
|
Loading…
Reference in a new issue