diff --git a/lib/apis/friend_api.dart b/lib/apis/friend_api.dart index 4402086..71bfb57 100644 --- a/lib/apis/friend_api.dart +++ b/lib/apis/friend_api.dart @@ -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> 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 addFriend(ApiClient client, {required User user}) async { - final response = await client.put("/users/${client.userId}/friends/${user.id}", body: user.toMap()); - ApiClient.checkResponse(response); - } } \ No newline at end of file diff --git a/lib/apis/user_api.dart b/lib/apis/user_api.dart index 53f29dd..1bd78d8 100644 --- a/lib/apis/user_api.dart +++ b/lib/apis/user_api.dart @@ -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> 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 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 removeUserAsFriend(ApiClient client, {required User user}) async { + final response = await client.delete("/users/${client.userId}/friends/${user.id}"); + ApiClient.checkResponse(response); + } } \ No newline at end of file diff --git a/lib/clients/api_client.dart b/lib/clients/api_client.dart index 18bf91a..be646b9 100644 --- a/lib/clients/api_client.dart +++ b/lib/clients/api_client.dart @@ -134,6 +134,7 @@ class ApiClient { Future put(String path, {Object? body, Map? headers}) { headers ??= {}; + headers["Content-Type"] = "application/json"; headers.addAll(authorizationHeader); return http.put(buildFullUri(path), headers: headers, body: body); } diff --git a/lib/models/friend.dart b/lib/models/friend.dart index a8c7861..2f0416a 100644 --- a/lib/models/friend.dart +++ b/lib/models/friend.dart @@ -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 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(),) + }; + } } \ No newline at end of file diff --git a/lib/models/session.dart b/lib/models/session.dart index c6f0ff3..991c493 100644 --- a/lib/models/session.dart +++ b/lib/models/session.dart @@ -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; } diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart index 2ff9bd6..e0b645a 100644 --- a/lib/models/user_profile.dart +++ b/lib/models/user_profile.dart @@ -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"] ?? ""); } diff --git a/lib/widgets/friends_list.dart b/lib/widgets/friends_list.dart index 3baff7e..725edab 100644 --- a/lib/widgets/friends_list.dart +++ b/lib/widgets/friends_list.dart @@ -110,10 +110,24 @@ class _FriendsListState extends State { _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( @@ -144,7 +158,7 @@ class _FriendsListState extends State { if (snapshot.hasData) { var friends = (snapshot.data as List); 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( diff --git a/lib/widgets/user_list_tile.dart b/lib/widgets/user_list_tile.dart index e1e48e4..9477dd5 100644 --- a/lib/widgets/user_list_tile.dart +++ b/lib/widgets/user_list_tile.dart @@ -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 createState() => _UserListTileState(); @@ -17,6 +20,7 @@ class UserListTile extends StatefulWidget { class _UserListTileState extends State { 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 { 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), diff --git a/lib/widgets/user_search.dart b/lib/widgets/user_search.dart index f5f5567..280079b 100644 --- a/lib/widgets/user_search.dart +++ b/lib/widgets/user_search.dart @@ -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 createState() => _UserSearchState(); @@ -71,7 +73,7 @@ class _UserSearchState extends State { 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) {