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