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/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);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"] ?? "");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue