Move all messaging state to MessagingClient and use Provider for state updates

This commit is contained in:
Nutcake 2023-05-06 16:45:26 +02:00
parent 43a451aad2
commit 766f5a94e2
9 changed files with 290 additions and 252 deletions

View file

@ -5,10 +5,10 @@ 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';
class FriendApi { class FriendApi {
static Future<Iterable<Friend>> getFriendsList(ApiClient client) async { static Future<List<Friend>> getFriendsList(ApiClient client) async {
final response = await client.get("/users/${client.userId}/friends"); final response = await client.get("/users/${client.userId}/friends");
ApiClient.checkResponse(response); ApiClient.checkResponse(response);
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)).toList();
} }
} }

View file

@ -4,7 +4,7 @@ import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/models/message.dart';
class MessageApi { class MessageApi {
static Future<Iterable<Message>> getUserMessages(ApiClient client, {String userId = "", DateTime? fromTime, static Future<List<Message>> getUserMessages(ApiClient client, {String userId = "", DateTime? fromTime,
int maxItems = 50, bool unreadOnly = false}) async { int maxItems = 50, bool unreadOnly = false}) async {
final response = await client.get("/users/${client.userId}/messages" final response = await client.get("/users/${client.userId}/messages"
@ -15,6 +15,6 @@ class MessageApi {
); );
ApiClient.checkResponse(response); ApiClient.checkResponse(response);
final data = jsonDecode(response.body) as List; final data = jsonDecode(response.body) as List;
return data.map((e) => Message.fromMap(e)); return data.map((e) => Message.fromMap(e)).toList();
} }
} }

View file

@ -1,6 +1,5 @@
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart';
import 'package:contacts_plus_plus/clients/notification_client.dart'; import 'package:contacts_plus_plus/clients/notification_client.dart';
import 'package:contacts_plus_plus/clients/settings_client.dart'; import 'package:contacts_plus_plus/clients/settings_client.dart';
import 'package:contacts_plus_plus/models/authentication_data.dart'; import 'package:contacts_plus_plus/models/authentication_data.dart';
@ -9,7 +8,6 @@ import 'package:flutter/material.dart';
class ClientHolder extends InheritedWidget { class ClientHolder extends InheritedWidget {
final ApiClient apiClient; final ApiClient apiClient;
final SettingsClient settingsClient; final SettingsClient settingsClient;
late final MessagingClient messagingClient;
final NotificationClient notificationClient = NotificationClient(); final NotificationClient notificationClient = NotificationClient();
ClientHolder({ ClientHolder({
@ -17,9 +15,7 @@ class ClientHolder extends InheritedWidget {
required AuthenticationData authenticationData, required AuthenticationData authenticationData,
required this.settingsClient, required this.settingsClient,
required super.child required super.child
}) : apiClient = ApiClient(authenticationData: authenticationData) { }) : apiClient = ApiClient(authenticationData: authenticationData);
messagingClient = MessagingClient(apiClient: apiClient, notificationClient: notificationClient);
}
static ClientHolder? maybeOf(BuildContext context) { static ClientHolder? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ClientHolder>(); return context.dependOnInheritedWidgetOfExactType<ClientHolder>();
@ -34,6 +30,5 @@ class ClientHolder extends InheritedWidget {
@override @override
bool updateShouldNotify(covariant ClientHolder oldWidget) => bool updateShouldNotify(covariant ClientHolder oldWidget) =>
oldWidget.apiClient != apiClient oldWidget.apiClient != apiClient
|| oldWidget.settingsClient != settingsClient || oldWidget.settingsClient != settingsClient;
|| oldWidget.messagingClient != messagingClient;
} }

View file

@ -1,9 +1,13 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart';
import 'package:contacts_plus_plus/apis/friend_api.dart';
import 'package:contacts_plus_plus/apis/message_api.dart'; import 'package:contacts_plus_plus/apis/message_api.dart';
import 'package:contacts_plus_plus/clients/notification_client.dart'; import 'package:contacts_plus_plus/clients/notification_client.dart';
import 'package:contacts_plus_plus/models/authentication_data.dart'; import 'package:contacts_plus_plus/models/authentication_data.dart';
import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/friend.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:contacts_plus_plus/clients/api_client.dart';
@ -37,39 +41,88 @@ enum EventTarget {
} }
} }
class MessagingClient { class MessagingClient extends ChangeNotifier {
static const String eofChar = ""; static const String eofChar = "";
static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$eofChar"; static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$eofChar";
static const List<int> _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60]; static const List<int> _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60];
static const String taskName = "periodic-unread-check"; static const String taskName = "periodic-unread-check";
static const Duration _autoRefreshDuration = Duration(seconds: 90);
static const Duration _refreshTimeoutDuration = Duration(seconds: 30);
final ApiClient _apiClient; final ApiClient _apiClient;
final Map<String, Friend> _friendsCache = {}; final Map<String, Friend> _friendsCache = {};
final List<Friend> _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build()
final Map<String, MessageCache> _messageCache = {}; final Map<String, MessageCache> _messageCache = {};
final Map<String, Function> _messageUpdateListeners = {}; final Map<String, Function> _messageUpdateListeners = {};
final Map<String, List<Message>> _unreads = {}; final Map<String, List<Message>> _unreads = {};
final Logger _logger = Logger("NeosHub"); final Logger _logger = Logger("NeosHub");
final Workmanager _workmanager = Workmanager(); final Workmanager _workmanager = Workmanager();
final NotificationClient _notificationClient; final NotificationClient _notificationClient;
Timer? _autoRefresh;
Timer? _refreshTimeout;
int _attempts = 0; int _attempts = 0;
Function? _unreadsUpdateListener;
WebSocket? _wsChannel; WebSocket? _wsChannel;
bool _isConnecting = false; bool _isConnecting = false;
String _initError = "";
bool _initDone = false;
String get initError => _initError;
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient}) MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
: _apiClient = apiClient, _notificationClient = notificationClient { : _apiClient = apiClient, _notificationClient = notificationClient {
refreshFriendsListWithErrorHandler();
start(); start();
} }
@override
void dispose() {
_autoRefresh?.cancel();
_refreshTimeout?.cancel();
_wsChannel?.close();
super.dispose();
}
void _sendData(data) { void _sendData(data) {
if (_wsChannel == null) throw "Neos Hub is not connected"; if (_wsChannel == null) throw "Neos Hub is not connected";
_wsChannel!.add(jsonEncode(data)+eofChar); _wsChannel!.add(jsonEncode(data)+eofChar);
} }
void updateFriendsCache(List<Friend> friends) { void refreshFriendsListWithErrorHandler () async {
try {
await refreshFriendsList();
_initDone = true;
} catch (e) {
_initError = "$e";
}
notifyListeners();
}
Future<void> refreshFriendsList() async {
if (_refreshTimeout?.isActive == true) return;
_autoRefresh?.cancel();
_autoRefresh = Timer(_autoRefreshDuration, () => refreshFriendsList());
_refreshTimeout?.cancel();
_refreshTimeout = Timer(_refreshTimeoutDuration, () {});
final unreadMessages = await MessageApi.getUserMessages(_apiClient, unreadOnly: true);
updateAllUnreads(unreadMessages.toList());
final friends = await FriendApi.getFriendsList(_apiClient);
_friendsCache.clear(); _friendsCache.clear();
for (final friend in friends) { for (final friend in friends) {
_friendsCache[friend.id] = friend; _friendsCache[friend.id] = friend;
} }
_sortedFriendsCache.clear();
_sortedFriendsCache.addAll(friends.sorted((a, b) {
var aVal = friendHasUnreads(a) ? -3 : 0;
var bVal = friendHasUnreads(b) ? -3 : 0;
aVal -= a.userStatus.lastStatusChange.compareTo(b.userStatus.lastStatusChange);
aVal += a.userStatus.onlineStatus.compareTo(b.userStatus.onlineStatus) * 2;
return aVal.compareTo(bVal);
}));
_initError = "";
notifyListeners();
} }
void updateAllUnreads(List<Message> messages) { void updateAllUnreads(List<Message> messages) {
@ -96,12 +149,12 @@ class MessagingClient {
} }
messages.sort(); messages.sort();
_notificationClient.showUnreadMessagesNotification(messages.reversed); _notificationClient.showUnreadMessagesNotification(messages.reversed);
notifyUnreadListener(); notifyListeners();
} }
void clearUnreadsForFriend(Friend friend) { void clearUnreadsForFriend(Friend friend) {
_unreads[friend.id]?.clear(); _unreads[friend.id]?.clear();
notifyUnreadListener(); notifyListeners();
} }
List<Message> getUnreadsForFriend(Friend friend) => _unreads[friend.id] ?? []; List<Message> getUnreadsForFriend(Friend friend) => _unreads[friend.id] ?? [];
@ -114,6 +167,8 @@ class MessagingClient {
Friend? getAsFriend(String userId) => _friendsCache[userId]; Friend? getAsFriend(String userId) => _friendsCache[userId];
List<Friend> get cachedFriends => _sortedFriendsCache;
Future<MessageCache> getMessageCache(String userId) async { Future<MessageCache> getMessageCache(String userId) async {
var cache = _messageCache[userId]; var cache = _messageCache[userId];
if (cache == null){ if (cache == null){
@ -204,10 +259,6 @@ class MessagingClient {
void unregisterMessageListener(String userId) => _messageUpdateListeners.remove(userId); void unregisterMessageListener(String userId) => _messageUpdateListeners.remove(userId);
void notifyMessageListener(String userId) => _messageUpdateListeners[userId]?.call(); void notifyMessageListener(String userId) => _messageUpdateListeners[userId]?.call();
void registerUnreadListener(Function function) => _unreadsUpdateListener = function;
void unregisterUnreadListener() => _unreadsUpdateListener = null;
void notifyUnreadListener() => _unreadsUpdateListener?.call();
void _handleEvent(event) { void _handleEvent(event) {
final body = jsonDecode((event.toString().replaceAll(eofChar, ""))); final body = jsonDecode((event.toString().replaceAll(eofChar, "")));
final int rawType = body["type"] ?? 0; final int rawType = body["type"] ?? 0;

View file

@ -9,6 +9,7 @@ import 'package:contacts_plus_plus/widgets/login_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:workmanager/workmanager.dart'; import 'package:workmanager/workmanager.dart';
import 'models/authentication_data.dart'; import 'models/authentication_data.dart';
@ -56,25 +57,35 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
return ClientHolder( return ClientHolder(
settingsClient: widget.settingsClient, settingsClient: widget.settingsClient,
authenticationData: _authData, authenticationData: _authData,
child: MaterialApp( child: Builder(
debugShowCheckedModeBanner: false, builder: (context) {
title: 'Contacts++', final clientHolder = ClientHolder.of(context);
theme: ThemeData( return MaterialApp(
useMaterial3: true, debugShowCheckedModeBanner: false,
textTheme: _typography.white, title: 'Contacts++',
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark) theme: ThemeData(
), useMaterial3: true,
home: _authData.isAuthenticated ? textTheme: _typography.white,
const FriendsList() : colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark)
LoginScreen( ),
onLoginSuccessful: (AuthenticationData authData) async { home: _authData.isAuthenticated ?
if (authData.isAuthenticated) { ChangeNotifierProvider(
setState(() { create: (context) =>
_authData = authData; MessagingClient(
}); apiClient: clientHolder.apiClient, notificationClient: clientHolder.notificationClient),
} child: const FriendsList(),
}, ) :
), LoginScreen(
onLoginSuccessful: (AuthenticationData authData) async {
if (authData.isAuthenticated) {
setState(() {
_authData = authData;
});
}
},
)
);
}
), ),
); );
} }

View file

@ -2,8 +2,7 @@ import 'dart:async';
import 'package:contacts_plus_plus/apis/user_api.dart'; import 'package:contacts_plus_plus/apis/user_api.dart';
import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/client_holder.dart';
import 'package:contacts_plus_plus/apis/friend_api.dart'; import 'package:contacts_plus_plus/clients/messaging_client.dart';
import 'package:contacts_plus_plus/apis/message_api.dart';
import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/friend.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/models/message.dart';
import 'package:contacts_plus_plus/models/personal_profile.dart'; import 'package:contacts_plus_plus/models/personal_profile.dart';
@ -15,6 +14,7 @@ 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'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class MenuItemDefinition { class MenuItemDefinition {
@ -33,72 +33,30 @@ class FriendsList extends StatefulWidget {
} }
class _FriendsListState extends State<FriendsList> { class _FriendsListState extends State<FriendsList> {
static const Duration _autoRefreshDuration = Duration(seconds: 90);
static const Duration _refreshTimeoutDuration = Duration(seconds: 30);
Future<List<Friend>>? _friendsFuture;
Future<PersonalProfile>? _userProfileFuture; Future<PersonalProfile>? _userProfileFuture;
Future<UserStatus>? _userStatusFuture; Future<UserStatus>? _userStatusFuture;
ClientHolder? _clientHolder; ClientHolder? _clientHolder;
Timer? _autoRefresh;
Timer? _refreshTimeout;
String _searchFilter = ""; String _searchFilter = "";
@override
void dispose() {
_autoRefresh?.cancel();
_refreshTimeout?.cancel();
super.dispose();
}
@override @override
void didChangeDependencies() async { void didChangeDependencies() async {
super.didChangeDependencies(); super.didChangeDependencies();
final clientHolder = ClientHolder.of(context); final clientHolder = ClientHolder.of(context);
if (_clientHolder != clientHolder) { if (_clientHolder != clientHolder) {
_clientHolder = clientHolder; _clientHolder = clientHolder;
final mClient = _clientHolder!.messagingClient;
mClient.registerUnreadListener(() {
if (context.mounted) {
setState(() {});
} else {
mClient.unregisterUnreadListener();
}
});
_refreshFriendsList();
final apiClient = _clientHolder!.apiClient; final apiClient = _clientHolder!.apiClient;
_userProfileFuture = UserApi.getPersonalProfile(apiClient); _userProfileFuture = UserApi.getPersonalProfile(apiClient);
_refreshUserStatus();
} }
} }
void _refreshFriendsList() { void _refreshUserStatus() {
if (_refreshTimeout?.isActive == true) return;
final apiClient = _clientHolder!.apiClient; 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());
final friends = value.toList()
..sort((a, b) {
var aVal = mClient.friendHasUnreads(a) ? -3 : 0;
var bVal = mClient.friendHasUnreads(b) ? -3 : 0;
aVal -= a.userStatus.lastStatusChange.compareTo(b.userStatus.lastStatusChange);
aVal += a.userStatus.onlineStatus.compareTo(b.userStatus.onlineStatus) * 2;
return aVal.compareTo(bVal);
});
_autoRefresh?.cancel();
_autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList()));
_refreshTimeout?.cancel();
_refreshTimeout = Timer(_refreshTimeoutDuration, () {});
_clientHolder?.messagingClient.updateFriendsCache(friends);
return friends;
});
_userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async { _userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async {
if (value.onlineStatus == OnlineStatus.offline) { if (value.onlineStatus == OnlineStatus.offline) {
final newStatus = value.copyWith( final newStatus = value.copyWith(
onlineStatus: OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus.valueOrDefault] onlineStatus: OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus
.valueOrDefault]
); );
await UserApi.setStatus(apiClient, status: newStatus); await UserApi.setStatus(apiClient, status: newStatus);
return newStatus; return newStatus;
@ -109,93 +67,107 @@ class _FriendsListState extends State<FriendsList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final apiClient = ClientHolder.of(context).apiClient; final clientHolder = ClientHolder.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Contacts++"), title: const Text("Contacts++"),
actions: [ actions: [
FutureBuilder( FutureBuilder(
future: _userStatusFuture, future: _userStatusFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final userStatus = snapshot.data as UserStatus; final userStatus = snapshot.data as UserStatus;
return PopupMenuButton<OnlineStatus>( return PopupMenuButton<OnlineStatus>(
child: Row( child: Row(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: Icon(Icons.circle, size: 16, color: userStatus.onlineStatus.color,), child: Icon(Icons.circle, size: 16, color: userStatus.onlineStatus.color,),
), ),
Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"), Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"),
], ],
), ),
onSelected: (OnlineStatus onlineStatus) async { onSelected: (OnlineStatus onlineStatus) async {
try { try {
final newStatus = userStatus.copyWith(onlineStatus: onlineStatus); final newStatus = userStatus.copyWith(onlineStatus: onlineStatus);
setState(() { setState(() {
_userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now())); _userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now()));
}); });
final settingsClient = ClientHolder.of(context).settingsClient; final settingsClient = ClientHolder
await UserApi.setStatus(apiClient, status: newStatus); .of(context)
await settingsClient.changeSettings(settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index)); .settingsClient;
} catch (e, s) { await UserApi.setStatus(clientHolder.apiClient, status: newStatus);
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); await settingsClient.changeSettings(
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to set online-status."))); settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index));
setState(() { } catch (e, s) {
_userStatusFuture = Future.value(userStatus); FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
}); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text(
} "Failed to set online-status.")));
}, setState(() {
itemBuilder: (BuildContext context) => _userStatusFuture = Future.value(userStatus);
OnlineStatus.values.where((element) => element != OnlineStatus.offline).map((item) => });
PopupMenuItem<OnlineStatus>( }
value: item, },
child: Row( itemBuilder: (BuildContext context) =>
mainAxisAlignment: MainAxisAlignment.start, OnlineStatus.values.where((element) => element != OnlineStatus.offline).map((item) =>
children: [ PopupMenuItem<OnlineStatus>(
Icon(Icons.circle, size: 16, color: item.color,), value: item,
const SizedBox(width: 8,), child: Row(
Text(toBeginningOfSentenceCase(item.name)!), mainAxisAlignment: MainAxisAlignment.start,
], children: [
Icon(Icons.circle, size: 16, color: item.color,),
const SizedBox(width: 8,),
Text(toBeginningOfSentenceCase(item.name)!),
],
),
), ),
), ).toList());
).toList()); } else if (snapshot.hasError) {
} else if (snapshot.hasError) { return TextButton.icon(
return TextButton.icon( style: TextButton.styleFrom(
style: TextButton.styleFrom( foregroundColor: Theme
foregroundColor: Theme.of(context).colorScheme.onSurface, .of(context)
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2) .colorScheme
), .onSurface,
onPressed: () { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2)
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,
), ),
), onPressed: () {
label: const Text("Loading"), setState(() {
); _userStatusFuture = null;
});
setState(() {
_userStatusFuture = UserApi.getUserStatus(clientHolder.apiClient, userId: clientHolder.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(left: 4, right: 4), padding: const EdgeInsets.only(left: 4, right: 4),
@ -210,17 +182,15 @@ class _FriendsListState extends State<FriendsList> {
name: "Settings", name: "Settings",
icon: Icons.settings, icon: Icons.settings,
onTap: () async { onTap: () async {
_autoRefresh?.cancel();
await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage())); await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage()));
_autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList()));
}, },
), ),
MenuItemDefinition( MenuItemDefinition(
name: "Find Users", name: "Find Users",
icon: Icons.person_add, icon: Icons.person_add,
onTap: () async { onTap: () async {
final mClient = Provider.of<MessagingClient>(context, listen: false);
bool changed = false; bool changed = false;
_autoRefresh?.cancel();
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) =>
@ -230,12 +200,7 @@ class _FriendsListState extends State<FriendsList> {
), ),
); );
if (changed) { if (changed) {
_refreshTimeout?.cancel(); mClient.refreshFriendsList();
setState(() {
_refreshFriendsList();
});
} else {
_autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList()));
} }
}, },
), ),
@ -247,24 +212,26 @@ class _FriendsListState extends State<FriendsList> {
context: context, context: context,
builder: (context) { builder: (context) {
return FutureBuilder( return FutureBuilder(
future: _userProfileFuture, future: _userProfileFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final profile = snapshot.data as PersonalProfile; final profile = snapshot.data as PersonalProfile;
return MyProfileDialog(profile: profile); return MyProfileDialog(profile: profile);
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
return DefaultErrorWidget( return DefaultErrorWidget(
title: "Failed to load personal profile.", title: "Failed to load personal profile.",
onRetry: () { onRetry: () {
setState(() { setState(() {
_userProfileFuture = UserApi.getPersonalProfile(ClientHolder.of(context).apiClient); _userProfileFuture = UserApi.getPersonalProfile(ClientHolder
}); .of(context)
}, .apiClient);
); });
} else { },
return const Center(child: CircularProgressIndicator(),); );
} else {
return const Center(child: CircularProgressIndicator(),);
}
} }
}
); );
}, },
); );
@ -288,62 +255,53 @@ class _FriendsListState extends State<FriendsList> {
), ),
body: Stack( body: Stack(
children: [ children: [
RefreshIndicator( Consumer<MessagingClient>(
onRefresh: () async { builder: (context, mClient, _) {
_refreshFriendsList(); if (mClient.initError.isNotEmpty) {
await _friendsFuture; // Keep the indicator running until everything's loaded return Column(
}, children: [
child: FutureBuilder( Expanded(
future: _friendsFuture, child: DefaultErrorWidget(
builder: (context, snapshot) { message: mClient.initError,
if (snapshot.hasData) { onRetry: () async {
var friends = (snapshot.data as List<Friend>); mClient.refreshFriendsListWithErrorHandler();
if (_searchFilter.isNotEmpty) { },
friends = friends.where((element) => )),
element.username.toLowerCase().contains(_searchFilter.toLowerCase())).toList(); ],
friends.sort((a, b) => a.username.length.compareTo(b.username.length)); );
} } else {
return ListView.builder( var friends = List.from(mClient.cachedFriends); // Explicit copy.
itemCount: friends.length, if (_searchFilter.isNotEmpty) {
itemBuilder: (context, index) { friends = friends.where((element) =>
final friend = friends[index]; element.username.toLowerCase().contains(_searchFilter.toLowerCase())).toList();
final unreads = _clientHolder?.messagingClient.getUnreadsForFriend(friend) ?? []; friends.sort((a, b) => a.username.length.compareTo(b.username.length));
return FriendListTile(
friend: friend,
unreads: unreads.length,
onTap: () async {
if (unreads.isNotEmpty) {
final readBatch = MarkReadBatch(
senderId: _clientHolder!.apiClient.userId,
ids: unreads.map((e) => e.id).toList(),
readTime: DateTime.now(),
);
_clientHolder!.messagingClient.markMessagesRead(readBatch);
}
setState(() {
unreads.clear();
});
},
);
},
);
} else if (snapshot.hasError) {
FlutterError.reportError(
FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
return DefaultErrorWidget(
message: "${snapshot.error}",
onRetry: () {
_refreshTimeout?.cancel();
setState(() {
_refreshFriendsList();
});
},
);
} else {
return const LinearProgressIndicator();
} }
return ListView.builder(
itemCount: friends.length,
itemBuilder: (context, index) {
final friend = friends[index];
final unreads = mClient.getUnreadsForFriend(friend);
return FriendListTile(
friend: friend,
unreads: unreads.length,
onTap: () async {
if (unreads.isNotEmpty) {
final readBatch = MarkReadBatch(
senderId: _clientHolder!.apiClient.userId,
ids: unreads.map((e) => e.id).toList(),
readTime: DateTime.now(),
);
mClient.markMessagesRead(readBatch);
}
setState(() {
unreads.clear();
});
},
);
},
);
} }
), }
), ),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,

View file

@ -47,6 +47,7 @@ class _MessagesListState extends State<MessagesList> {
void _loadMessages() { void _loadMessages() {
_messageCacheFutureComplete = false; _messageCacheFutureComplete = false;
/* TODO: Use provider
_messageCacheFuture = _clientHolder?.messagingClient.getMessageCache(widget.friend.id) _messageCacheFuture = _clientHolder?.messagingClient.getMessageCache(widget.friend.id)
.whenComplete(() => _messageCacheFutureComplete = true); .whenComplete(() => _messageCacheFutureComplete = true);
final mClient = _clientHolder?.messagingClient; final mClient = _clientHolder?.messagingClient;
@ -58,11 +59,13 @@ class _MessagesListState extends State<MessagesList> {
mClient.unregisterMessageListener(id); mClient.unregisterMessageListener(id);
} }
}); });
*/
} }
@override @override
void dispose() { void dispose() {
_clientHolder?.messagingClient.unregisterMessageListener(widget.friend.id); // TODO user provider
//_clientHolder?.messagingClient.unregisterMessageListener(widget.friend.id);
_messageTextController.dispose(); _messageTextController.dispose();
_sessionListScrollController.dispose(); _sessionListScrollController.dispose();
super.dispose(); super.dispose();
@ -89,8 +92,9 @@ class _MessagesListState extends State<MessagesList> {
_messageScrollController.position.maxScrollExtent > 0 && _messageCacheFutureComplete) { _messageScrollController.position.maxScrollExtent > 0 && _messageCacheFutureComplete) {
setState(() { setState(() {
_messageCacheFutureComplete = false; _messageCacheFutureComplete = false;
_messageCacheFuture = _clientHolder?.messagingClient.getMessageCache(widget.friend.id) // TODO: Use provider
.then((value) => value.loadOlderMessages()).whenComplete(() => _messageCacheFutureComplete = true); //_messageCacheFuture = _clientHolder?.messagingClient.getMessageCache(widget.friend.id)
// .then((value) => value.loadOlderMessages()).whenComplete(() => _messageCacheFutureComplete = true);
}); });
} }
}); });
@ -280,7 +284,8 @@ class _MessagesListState extends State<MessagesList> {
sendTime: DateTime.now().toUtc(), sendTime: DateTime.now().toUtc(),
); );
try { try {
_clientHolder!.messagingClient.sendMessage(message); // TODO use provider
//_clientHolder!.messagingClient.sendMessage(message);
_messageTextController.clear(); _messageTextController.clear();
setState(() {}); setState(() {});
} catch (e) { } catch (e) {

View file

@ -53,9 +53,11 @@ class _UserSearchState extends State<UserSearch> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
/* TODO: Use provider
final mClient = ClientHolder final mClient = ClientHolder
.of(context) .of(context)
.messagingClient; .messagingClient;
*/
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Find Users"), title: const Text("Find Users"),
@ -72,7 +74,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, onChange: widget.onFriendsChanged); return UserListTile(user: user, onChange: widget.onFriendsChanged, isFriend: false,); // TODO: Use provider mClient.getAsFriend(user.id) != null,);
}, },
); );
} else if (snapshot.hasError) { } else if (snapshot.hasError) {

View file

@ -368,6 +368,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.8.0" version: "1.8.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
octo_image: octo_image:
dependency: transitive dependency: transitive
description: description:
@ -488,6 +496,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.4" version: "4.2.4"
provider:
dependency: "direct main"
description:
name: provider
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
url: "https://pub.dev"
source: hosted
version: "6.0.5"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description: