Add user status switcher

This commit is contained in:
Nutcake 2023-05-05 16:39:40 +02:00
parent 1e336688b7
commit 1676e6d19d
5 changed files with 158 additions and 18 deletions

View file

@ -21,6 +21,19 @@ class UserApi {
return User.fromMap(data); return User.fromMap(data);
} }
static Future<UserStatus> getUserStatus(ApiClient client, {required String userId}) async {
final response = await client.get("/users/$userId/status");
ApiClient.checkResponse(response);
final data = jsonDecode(response.body);
return UserStatus.fromMap(data);
}
static Future<void> setStatus(ApiClient client, {required UserStatus status}) async {
final body = jsonEncode(status.toMap(shallow: true));
final response = await client.put("/users/${client.userId}/status", body: body);
ApiClient.checkResponse(response);
}
static Future<PersonalProfile> getPersonalProfile(ApiClient client) async { static Future<PersonalProfile> getPersonalProfile(ApiClient client) async {
final response = await client.get("/users/${client.userId}"); final response = await client.get("/users/${client.userId}");
ApiClient.checkResponse(response); ApiClient.checkResponse(response);

View file

@ -7,7 +7,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SettingsClient { class SettingsClient {
static const String _settingsKey = "settings"; static const String _settingsKey = "settings";
static const _storage = FlutterSecureStorage(); static const _storage = FlutterSecureStorage();
Settings _currentSettings = const Settings(); Settings _currentSettings = Settings();
Settings get currentSettings => _currentSettings; Settings get currentSettings => _currentSettings;

View file

@ -2,6 +2,7 @@ import 'dart:developer';
import 'package:contacts_plus_plus/models/session.dart'; import 'package:contacts_plus_plus/models/session.dart';
import 'package:contacts_plus_plus/models/user_profile.dart'; import 'package:contacts_plus_plus/models/user_profile.dart';
import 'package:flutter/material.dart';
class Friend extends Comparable { class Friend extends Comparable {
final String id; final String id;
@ -59,15 +60,25 @@ enum FriendStatus {
} }
enum OnlineStatus { enum OnlineStatus {
unknown,
offline, offline,
invisible,
away, away,
busy, busy,
online; online;
static final List<Color> _colors = [
Colors.black54,
Colors.white54,
Colors.yellow,
Colors.red,
Colors.green,
];
Color get color => _colors[index];
factory OnlineStatus.fromString(String? text) { factory OnlineStatus.fromString(String? text) {
return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(), return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
orElse: () => OnlineStatus.unknown, orElse: () => OnlineStatus.offline,
); );
} }
@ -91,7 +102,7 @@ class UserStatus {
UserStatus({required this.onlineStatus, required this.lastStatusChange, required this.activeSessions}); UserStatus({required this.onlineStatus, required this.lastStatusChange, required this.activeSessions});
factory UserStatus.empty() => UserStatus( factory UserStatus.empty() => UserStatus(
onlineStatus: OnlineStatus.unknown, onlineStatus: OnlineStatus.offline,
lastStatusChange: DateTime.now(), lastStatusChange: DateTime.now(),
activeSessions: [], activeSessions: [],
); );
@ -99,9 +110,6 @@ class UserStatus {
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);
if (status == OnlineStatus.unknown && statusString != null) {
log("Unknown OnlineStatus '$statusString' in response");
}
return UserStatus( return UserStatus(
onlineStatus: status, onlineStatus: status,
lastStatusChange: DateTime.parse(map["lastStatusChange"]), lastStatusChange: DateTime.parse(map["lastStatusChange"]),
@ -116,4 +124,11 @@ class UserStatus {
"activeSessions": shallow ? [] : activeSessions.map((e) => e.toMap(),) "activeSessions": shallow ? [] : activeSessions.map((e) => e.toMap(),)
}; };
} }
UserStatus copyWith({OnlineStatus? onlineStatus, DateTime? lastStatusChange, List<Session>? activeSessions})
=> UserStatus(
onlineStatus: onlineStatus ?? this.onlineStatus,
lastStatusChange: lastStatusChange ?? this.lastStatusChange,
activeSessions: activeSessions ?? this.activeSessions,
);
} }

View file

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:contacts_plus_plus/models/friend.dart';
class SettingsEntry<T> { class SettingsEntry<T> {
final T? value; final T? value;
final T deflt; final T deflt;
@ -32,32 +34,44 @@ class SettingsEntry<T> {
class Settings { class Settings {
final SettingsEntry<bool> notificationsDenied; final SettingsEntry<bool> notificationsDenied;
final SettingsEntry<int> unreadCheckIntervalMinutes; final SettingsEntry<int> unreadCheckIntervalMinutes;
final SettingsEntry<int> lastOnlineStatus;
const Settings({ Settings({
this.notificationsDenied = const SettingsEntry(deflt: false), SettingsEntry<bool>? notificationsDenied,
this.unreadCheckIntervalMinutes = const SettingsEntry(deflt: 60), SettingsEntry<int>? unreadCheckIntervalMinutes,
}); SettingsEntry<int>? lastOnlineStatus,
}) : notificationsDenied = notificationsDenied ?? const SettingsEntry(deflt: false),
unreadCheckIntervalMinutes = unreadCheckIntervalMinutes ?? const SettingsEntry(deflt: 60),
lastOnlineStatus = lastOnlineStatus ?? SettingsEntry(deflt: OnlineStatus.online.index);
factory Settings.fromMap(Map map) { factory Settings.fromMap(Map map) {
return Settings( return Settings(
notificationsDenied: SettingsEntry.fromMap(map["notificationsDenied"]), notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]),
unreadCheckIntervalMinutes: SettingsEntry.fromMap(map["unreadCheckIntervalMinutes"]), unreadCheckIntervalMinutes: retrieveEntryOrNull<int>(map["unreadCheckIntervalMinutes"]),
lastOnlineStatus: retrieveEntryOrNull<int>(map["lastOnlineStatus"]),
); );
} }
static SettingsEntry<T>? retrieveEntryOrNull<T>(Map? map) {
if (map == null) return null;
return SettingsEntry<T>.fromMap(map);
}
Map toMap() { Map toMap() {
return { return {
"notificationsDenied": notificationsDenied.toMap(), "notificationsDenied": notificationsDenied.toMap(),
"unreadCheckIntervalMinutes": unreadCheckIntervalMinutes.toMap(), "unreadCheckIntervalMinutes": unreadCheckIntervalMinutes.toMap(),
"lastOnlineStatus": lastOnlineStatus.toMap(),
}; };
} }
Settings copy() => copyWith(); Settings copy() => copyWith();
Settings copyWith({bool? notificationsDenied, int? unreadCheckIntervalMinutes}) { Settings copyWith({bool? notificationsDenied, int? unreadCheckIntervalMinutes, int? lastOnlineStatus}) {
return Settings( return Settings(
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied), notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
unreadCheckIntervalMinutes: this.unreadCheckIntervalMinutes.passThrough(unreadCheckIntervalMinutes), unreadCheckIntervalMinutes: this.unreadCheckIntervalMinutes.passThrough(unreadCheckIntervalMinutes),
lastOnlineStatus: this.lastOnlineStatus.passThrough(lastOnlineStatus),
); );
} }

View file

@ -14,6 +14,7 @@ import 'package:contacts_plus_plus/widgets/my_profile_dialog.dart';
import 'package:contacts_plus_plus/widgets/settings_page.dart'; import 'package:contacts_plus_plus/widgets/settings_page.dart';
import 'package:contacts_plus_plus/widgets/user_search.dart'; import 'package:contacts_plus_plus/widgets/user_search.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class MenuItemDefinition { class MenuItemDefinition {
@ -36,6 +37,7 @@ class _FriendsListState extends State<FriendsList> {
static const Duration _refreshTimeoutDuration = Duration(seconds: 30); static const Duration _refreshTimeoutDuration = Duration(seconds: 30);
Future<List<Friend>>? _friendsFuture; Future<List<Friend>>? _friendsFuture;
Future<PersonalProfile>? _userProfileFuture; Future<PersonalProfile>? _userProfileFuture;
Future<UserStatus>? _userStatusFuture;
ClientHolder? _clientHolder; ClientHolder? _clientHolder;
Timer? _autoRefresh; Timer? _autoRefresh;
Timer? _refreshTimeout; Timer? _refreshTimeout;
@ -63,14 +65,16 @@ class _FriendsListState extends State<FriendsList> {
} }
}); });
_refreshFriendsList(); _refreshFriendsList();
_userProfileFuture = UserApi.getPersonalProfile(_clientHolder!.apiClient); final apiClient = _clientHolder!.apiClient;
_userProfileFuture = UserApi.getPersonalProfile(apiClient);
} }
} }
void _refreshFriendsList() { void _refreshFriendsList() {
if (_refreshTimeout?.isActive == true) return; if (_refreshTimeout?.isActive == true) return;
_friendsFuture = FriendApi.getFriendsList(_clientHolder!.apiClient).then((Iterable<Friend> value) async { final apiClient = _clientHolder!.apiClient;
final unreadMessages = await MessageApi.getUserMessages(_clientHolder!.apiClient, unreadOnly: true); _friendsFuture = FriendApi.getFriendsList(apiClient).then((Iterable<Friend> value) async {
final unreadMessages = await MessageApi.getUserMessages(apiClient, unreadOnly: true);
final mClient = _clientHolder?.messagingClient; final mClient = _clientHolder?.messagingClient;
if (mClient == null) return []; if (mClient == null) return [];
mClient.updateAllUnreads(unreadMessages.toList()); mClient.updateAllUnreads(unreadMessages.toList());
@ -91,16 +95,110 @@ class _FriendsListState extends State<FriendsList> {
_clientHolder?.messagingClient.updateFriendsCache(friends); _clientHolder?.messagingClient.updateFriendsCache(friends);
return friends; return friends;
}); });
_userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async {
if (value.onlineStatus == OnlineStatus.offline) {
final newStatus = value.copyWith(
onlineStatus: OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus.valueOrDefault]
);
await UserApi.setStatus(apiClient, status: newStatus);
return newStatus;
}
return value;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final apiClient = ClientHolder.of(context).apiClient;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Contacts++"), title: const Text("Contacts++"),
actions: [ actions: [
FutureBuilder(
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(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) =>
OnlineStatus.values.where((element) => element != OnlineStatus.offline).map((item) =>
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)
),
onPressed: () {
setState(() {
_userStatusFuture = null;
});
setState(() {
_userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId);
});
},
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,
),
),
label: const Text("Loading"),
);
}
}
),
Padding( Padding(
padding: const EdgeInsets.only(right: 4), padding: const EdgeInsets.only(left: 4, right: 4),
child: PopupMenuButton<MenuItemDefinition>( child: PopupMenuButton<MenuItemDefinition>(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
onSelected: (MenuItemDefinition itemDef) async { onSelected: (MenuItemDefinition itemDef) async {