Add user status switcher
This commit is contained in:
parent
1e336688b7
commit
1676e6d19d
5 changed files with 158 additions and 18 deletions
|
@ -21,6 +21,19 @@ class UserApi {
|
|||
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 {
|
||||
final response = await client.get("/users/${client.userId}");
|
||||
ApiClient.checkResponse(response);
|
||||
|
|
|
@ -7,7 +7,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||
class SettingsClient {
|
||||
static const String _settingsKey = "settings";
|
||||
static const _storage = FlutterSecureStorage();
|
||||
Settings _currentSettings = const Settings();
|
||||
Settings _currentSettings = Settings();
|
||||
|
||||
Settings get currentSettings => _currentSettings;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:developer';
|
|||
|
||||
import 'package:contacts_plus_plus/models/session.dart';
|
||||
import 'package:contacts_plus_plus/models/user_profile.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Friend extends Comparable {
|
||||
final String id;
|
||||
|
@ -59,15 +60,25 @@ enum FriendStatus {
|
|||
}
|
||||
|
||||
enum OnlineStatus {
|
||||
unknown,
|
||||
offline,
|
||||
invisible,
|
||||
away,
|
||||
busy,
|
||||
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) {
|
||||
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});
|
||||
|
||||
factory UserStatus.empty() => UserStatus(
|
||||
onlineStatus: OnlineStatus.unknown,
|
||||
onlineStatus: OnlineStatus.offline,
|
||||
lastStatusChange: DateTime.now(),
|
||||
activeSessions: [],
|
||||
);
|
||||
|
@ -99,9 +110,6 @@ class UserStatus {
|
|||
factory UserStatus.fromMap(Map map) {
|
||||
final statusString = map["onlineStatus"] as String?;
|
||||
final status = OnlineStatus.fromString(statusString);
|
||||
if (status == OnlineStatus.unknown && statusString != null) {
|
||||
log("Unknown OnlineStatus '$statusString' in response");
|
||||
}
|
||||
return UserStatus(
|
||||
onlineStatus: status,
|
||||
lastStatusChange: DateTime.parse(map["lastStatusChange"]),
|
||||
|
@ -116,4 +124,11 @@ class UserStatus {
|
|||
"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,
|
||||
);
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:contacts_plus_plus/models/friend.dart';
|
||||
|
||||
class SettingsEntry<T> {
|
||||
final T? value;
|
||||
final T deflt;
|
||||
|
@ -32,32 +34,44 @@ class SettingsEntry<T> {
|
|||
class Settings {
|
||||
final SettingsEntry<bool> notificationsDenied;
|
||||
final SettingsEntry<int> unreadCheckIntervalMinutes;
|
||||
final SettingsEntry<int> lastOnlineStatus;
|
||||
|
||||
const Settings({
|
||||
this.notificationsDenied = const SettingsEntry(deflt: false),
|
||||
this.unreadCheckIntervalMinutes = const SettingsEntry(deflt: 60),
|
||||
});
|
||||
Settings({
|
||||
SettingsEntry<bool>? notificationsDenied,
|
||||
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) {
|
||||
return Settings(
|
||||
notificationsDenied: SettingsEntry.fromMap(map["notificationsDenied"]),
|
||||
unreadCheckIntervalMinutes: SettingsEntry.fromMap(map["unreadCheckIntervalMinutes"]),
|
||||
notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]),
|
||||
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() {
|
||||
return {
|
||||
"notificationsDenied": notificationsDenied.toMap(),
|
||||
"unreadCheckIntervalMinutes": unreadCheckIntervalMinutes.toMap(),
|
||||
"lastOnlineStatus": lastOnlineStatus.toMap(),
|
||||
};
|
||||
}
|
||||
|
||||
Settings copy() => copyWith();
|
||||
|
||||
Settings copyWith({bool? notificationsDenied, int? unreadCheckIntervalMinutes}) {
|
||||
Settings copyWith({bool? notificationsDenied, int? unreadCheckIntervalMinutes, int? lastOnlineStatus}) {
|
||||
return Settings(
|
||||
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
|
||||
unreadCheckIntervalMinutes: this.unreadCheckIntervalMinutes.passThrough(unreadCheckIntervalMinutes),
|
||||
lastOnlineStatus: this.lastOnlineStatus.passThrough(lastOnlineStatus),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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/user_search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
|
||||
class MenuItemDefinition {
|
||||
|
@ -36,6 +37,7 @@ class _FriendsListState extends State<FriendsList> {
|
|||
static const Duration _refreshTimeoutDuration = Duration(seconds: 30);
|
||||
Future<List<Friend>>? _friendsFuture;
|
||||
Future<PersonalProfile>? _userProfileFuture;
|
||||
Future<UserStatus>? _userStatusFuture;
|
||||
ClientHolder? _clientHolder;
|
||||
Timer? _autoRefresh;
|
||||
Timer? _refreshTimeout;
|
||||
|
@ -63,14 +65,16 @@ class _FriendsListState extends State<FriendsList> {
|
|||
}
|
||||
});
|
||||
_refreshFriendsList();
|
||||
_userProfileFuture = UserApi.getPersonalProfile(_clientHolder!.apiClient);
|
||||
final apiClient = _clientHolder!.apiClient;
|
||||
_userProfileFuture = UserApi.getPersonalProfile(apiClient);
|
||||
}
|
||||
}
|
||||
|
||||
void _refreshFriendsList() {
|
||||
if (_refreshTimeout?.isActive == true) return;
|
||||
_friendsFuture = FriendApi.getFriendsList(_clientHolder!.apiClient).then((Iterable<Friend> value) async {
|
||||
final unreadMessages = await MessageApi.getUserMessages(_clientHolder!.apiClient, unreadOnly: true);
|
||||
final apiClient = _clientHolder!.apiClient;
|
||||
_friendsFuture = FriendApi.getFriendsList(apiClient).then((Iterable<Friend> value) async {
|
||||
final unreadMessages = await MessageApi.getUserMessages(apiClient, unreadOnly: true);
|
||||
final mClient = _clientHolder?.messagingClient;
|
||||
if (mClient == null) return [];
|
||||
mClient.updateAllUnreads(unreadMessages.toList());
|
||||
|
@ -91,16 +95,110 @@ class _FriendsListState extends State<FriendsList> {
|
|||
_clientHolder?.messagingClient.updateFriendsCache(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
|
||||
Widget build(BuildContext context) {
|
||||
final apiClient = ClientHolder.of(context).apiClient;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Contacts++"),
|
||||
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: const EdgeInsets.only(right: 4),
|
||||
padding: const EdgeInsets.only(left: 4, right: 4),
|
||||
child: PopupMenuButton<MenuItemDefinition>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (MenuItemDefinition itemDef) async {
|
||||
|
|
Loading…
Reference in a new issue