Make messages-list use new Provider implementation

This commit is contained in:
Nutcake 2023-05-06 17:47:36 +02:00
parent 33e46c9f22
commit ab21717829
6 changed files with 147 additions and 180 deletions

View file

@ -52,11 +52,11 @@ class MessagingClient extends ChangeNotifier {
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 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, 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;
Friend? selectedFriend;
Timer? _autoRefresh; Timer? _autoRefresh;
Timer? _refreshTimeout; Timer? _refreshTimeout;
int _attempts = 0; int _attempts = 0;
@ -172,16 +172,17 @@ class MessagingClient extends ChangeNotifier {
List<Friend> get cachedFriends => _sortedFriendsCache; List<Friend> get cachedFriends => _sortedFriendsCache;
Future<MessageCache> getMessageCache(String userId) async { MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId);
var cache = _messageCache[userId];
if (cache == null){ Future<void> loadUserMessageCache(String userId) async {
cache = MessageCache(apiClient: _apiClient, userId: userId); final cache = getUserMessageCache(userId) ?? _createUserMessageCache(userId);
await cache.loadInitialMessages(); await cache.loadMessages();
_messageCache[userId] = cache; _messageCache[userId] = cache;
} notifyListeners();
return cache;
} }
MessageCache? getUserMessageCache(String userId) => _messageCache[userId];
static Future<void> backgroundCheckUnreads(Map<String, dynamic>? inputData) async { static Future<void> backgroundCheckUnreads(Map<String, dynamic>? inputData) async {
if (inputData == null) return; if (inputData == null) return;
final auth = AuthenticationData.fromMap(inputData); final auth = AuthenticationData.fromMap(inputData);
@ -258,10 +259,6 @@ class MessagingClient extends ChangeNotifier {
} }
} }
void registerMessageListener(String userId, Function function) => _messageUpdateListeners[userId] = function;
void unregisterMessageListener(String userId) => _messageUpdateListeners.remove(userId);
void notifyMessageListener(String userId) => _messageUpdateListeners[userId]?.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;
@ -301,28 +298,30 @@ class MessagingClient extends ChangeNotifier {
case EventTarget.messageSent: case EventTarget.messageSent:
final msg = args[0]; final msg = args[0];
final message = Message.fromMap(msg, withState: MessageState.sent); final message = Message.fromMap(msg, withState: MessageState.sent);
final cache = await getMessageCache(message.recipientId); final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.addMessage(message); cache.addMessage(message);
notifyMessageListener(message.recipientId); notifyListeners();
break; break;
case EventTarget.receiveMessage: case EventTarget.receiveMessage:
final msg = args[0]; final msg = args[0];
final message = Message.fromMap(msg); final message = Message.fromMap(msg);
final cache = await getMessageCache(message.senderId); final cache = getUserMessageCache(message.senderId) ?? _createUserMessageCache(message.senderId);
cache.addMessage(message); cache.addMessage(message);
if (!_messageUpdateListeners.containsKey(message.senderId)) { if (message.senderId != selectedFriend?.id) {
addUnread(message); addUnread(message);
} }
notifyMessageListener(message.senderId); notifyListeners();
break; break;
case EventTarget.messagesRead: case EventTarget.messagesRead:
final messageIds = args[0]["ids"] as List; final messageIds = args[0]["ids"] as List;
final recipientId = args[0]["recipientId"]; final recipientId = args[0]["recipientId"];
final cache = await getMessageCache(recipientId ?? ""); if (recipientId == null) break;
final cache = getUserMessageCache(recipientId);
if (cache == null) break;
for (var id in messageIds) { for (var id in messageIds) {
cache.setMessageState(id, MessageState.read); cache.setMessageState(id, MessageState.read);
} }
notifyMessageListener(recipientId); notifyListeners();
break; break;
} }
} }
@ -337,9 +336,9 @@ class MessagingClient extends ChangeNotifier {
], ],
}; };
_sendData(data); _sendData(data);
final cache = await getMessageCache(message.recipientId); final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.messages.add(message); cache.messages.add(message);
notifyMessageListener(message.recipientId); notifyListeners();
} }
void markMessagesRead(MarkReadBatch batch) { void markMessagesRead(MarkReadBatch batch) {
@ -354,3 +353,8 @@ class MessagingClient extends ChangeNotifier {
_sendData(data); _sendData(data);
} }
} }
class MessagesProvider extends ChangeNotifier {
}

View file

@ -69,7 +69,7 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark) colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark)
), ),
home: _authData.isAuthenticated ? home: _authData.isAuthenticated ?
ChangeNotifierProvider( ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime.
create: (context) => create: (context) =>
MessagingClient( MessagingClient(
apiClient: clientHolder.apiClient, notificationClient: clientHolder.notificationClient), apiClient: clientHolder.apiClient, notificationClient: clientHolder.notificationClient),

View file

@ -140,7 +140,7 @@ class MessageCache {
return this; //lmao return this; //lmao
} }
Future<void> loadInitialMessages() async { Future<void> loadMessages() async {
final messages = await MessageApi.getUserMessages(_apiClient, userId: _userId); final messages = await MessageApi.getUserMessages(_apiClient, userId: _userId);
_messages.addAll(messages); _messages.addAll(messages);
_ensureIntegrity(); _ensureIntegrity();

View file

@ -1,9 +1,13 @@
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/client_holder.dart';
import 'package:contacts_plus_plus/clients/messaging_client.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/widgets/generic_avatar.dart'; import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
import 'package:contacts_plus_plus/widgets/messages_list.dart'; import 'package:contacts_plus_plus/widgets/messages_list.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 FriendListTile extends StatelessWidget { class FriendListTile extends StatelessWidget {
const FriendListTile({required this.friend, this.unreads, this.onTap, super.key}); const FriendListTile({required this.friend, this.unreads, this.onTap, super.key});
@ -24,8 +28,32 @@ class FriendListTile extends StatelessWidget {
title: Text(friend.username), title: Text(friend.username),
subtitle: Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"), subtitle: Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"),
onTap: () async { onTap: () async {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => MessagesList(friend: friend))); onTap?.call();
await onTap?.call(); final mClient = Provider.of<MessagingClient>(context, listen: false);
mClient.loadUserMessageCache(friend.id);
final apiClient = ClientHolder
.of(context)
.apiClient;
final unreads = mClient.getUnreadsForFriend(friend);
if (unreads.isNotEmpty) {
final readBatch = MarkReadBatch(
senderId: apiClient.userId,
ids: unreads.map((e) => e.id).toList(),
readTime: DateTime.now(),
);
mClient.markMessagesRead(readBatch);
}
mClient.selectedFriend = friend;
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
ChangeNotifierProvider<MessagingClient>.value(
value: mClient,
child: MessagesList(friend: friend),
),
),
);
mClient.selectedFriend = null;
}, },
); );
} }

View file

@ -289,19 +289,6 @@ class _FriendsListState extends State<FriendsList> {
return FriendListTile( return FriendListTile(
friend: friend, friend: friend,
unreads: unreads.length, 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();
});
},
); );
}, },
); );

View file

@ -1,17 +1,16 @@
import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/client_holder.dart';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/clients/messaging_client.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/session.dart'; import 'package:contacts_plus_plus/models/session.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/message_audio_player.dart'; import 'package:contacts_plus_plus/widgets/message_audio_player.dart';
import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
import 'package:contacts_plus_plus/widgets/message_session_invite.dart'; import 'package:contacts_plus_plus/widgets/message_session_invite.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 MessagesList extends StatefulWidget { class MessagesList extends StatefulWidget {
const MessagesList({required this.friend, super.key}); const MessagesList({required this.friend, super.key});
@ -23,49 +22,18 @@ class MessagesList extends StatefulWidget {
} }
class _MessagesListState extends State<MessagesList> { class _MessagesListState extends State<MessagesList> {
Future<MessageCache>? _messageCacheFuture;
final TextEditingController _messageTextController = TextEditingController(); final TextEditingController _messageTextController = TextEditingController();
final ScrollController _sessionListScrollController = ScrollController(); final ScrollController _sessionListScrollController = ScrollController();
final ScrollController _messageScrollController = ScrollController(); final ScrollController _messageScrollController = ScrollController();
ClientHolder? _clientHolder;
bool _isSendable = false; bool _isSendable = false;
bool _showSessionListChevron = false; bool _showSessionListScrollChevron = false;
bool _messageCacheFutureComplete = false;
double get _shevronOpacity => _showSessionListChevron ? 1.0 : 0.0; double get _shevronOpacity => _showSessionListScrollChevron ? 1.0 : 0.0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final clientHolder = ClientHolder.of(context);
if (_clientHolder != clientHolder) {
_clientHolder = clientHolder;
}
_loadMessages();
}
void _loadMessages() {
_messageCacheFutureComplete = false;
/* TODO: Use provider
_messageCacheFuture = _clientHolder?.messagingClient.getMessageCache(widget.friend.id)
.whenComplete(() => _messageCacheFutureComplete = true);
final mClient = _clientHolder?.messagingClient;
final id = widget.friend.id;
mClient?.registerMessageListener(id, () {
if (context.mounted) {
setState(() {});
} else {
mClient.unregisterMessageListener(id);
}
});
*/
}
@override @override
void dispose() { void dispose() {
// TODO user provider
//_clientHolder?.messagingClient.unregisterMessageListener(widget.friend.id);
_messageTextController.dispose(); _messageTextController.dispose();
_sessionListScrollController.dispose(); _sessionListScrollController.dispose();
super.dispose(); super.dispose();
@ -75,26 +43,15 @@ class _MessagesListState extends State<MessagesList> {
void initState() { void initState() {
super.initState(); super.initState();
_sessionListScrollController.addListener(() { _sessionListScrollController.addListener(() {
if (_sessionListScrollController.position.maxScrollExtent > 0 && !_showSessionListChevron) { if (_sessionListScrollController.position.maxScrollExtent > 0 && !_showSessionListScrollChevron) {
setState(() { setState(() {
_showSessionListChevron = true; _showSessionListScrollChevron = true;
}); });
} }
if (_sessionListScrollController.position.atEdge && _sessionListScrollController.position.pixels > 0 if (_sessionListScrollController.position.atEdge && _sessionListScrollController.position.pixels > 0
&& _showSessionListChevron) { && _showSessionListScrollChevron) {
setState(() { setState(() {
_showSessionListChevron = false; _showSessionListScrollChevron = false;
});
}
});
_messageScrollController.addListener(() {
if (_messageScrollController.position.atEdge && _messageScrollController.position.pixels > 0 &&
_messageScrollController.position.maxScrollExtent > 0 && _messageCacheFutureComplete) {
setState(() {
_messageCacheFutureComplete = false;
// TODO: Use provider
//_messageCacheFuture = _clientHolder?.messagingClient.getMessageCache(widget.friend.id)
// .then((value) => value.loadOlderMessages()).whenComplete(() => _messageCacheFutureComplete = true);
}); });
} }
}); });
@ -160,68 +117,56 @@ class _MessagesListState extends State<MessagesList> {
), ),
), ),
Expanded( Expanded(
child: FutureBuilder( child: Consumer<MessagingClient>(
future: _messageCacheFuture, builder: (context, mClient, _) {
builder: (context, snapshot) { final cache = mClient.getUserMessageCache(widget.friend.id);
if (snapshot.hasData) { if (cache == null) {
final cache = snapshot.data as MessageCache;
if (cache.messages.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.message_outlined),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Text(
"There are no messages here\nWhy not say hello?",
textAlign: TextAlign.center,
style: Theme
.of(context)
.textTheme
.titleMedium,
),
)
],
),
);
}
return ListView.builder(
controller: _messageScrollController,
reverse: true,
itemCount: cache.messages.length,
itemBuilder: (context, index) {
final entry = cache.messages[index];
final widget = entry.senderId == apiClient.userId
? MyMessageBubble(message: entry)
: OtherMessageBubble(message: entry);
if (index == cache.messages.length-1) {
return Padding(
padding: const EdgeInsets.only(top: 12),
child: widget,
);
}
return widget;
},
);
} else if (snapshot.hasError) {
return DefaultErrorWidget(
message: "${snapshot.error}",
onRetry: () {
setState(() {
_loadMessages();
});
},
);
} else {
return Column( return Column(
mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: const [ children: const [
LinearProgressIndicator(), LinearProgressIndicator()
], ],
); );
} }
if (cache.messages.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.message_outlined),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Text(
"There are no messages here\nWhy not say hello?",
textAlign: TextAlign.center,
style: Theme
.of(context)
.textTheme
.titleMedium,
),
)
],
),
);
}
return ListView.builder(
controller: _messageScrollController,
reverse: true,
itemCount: cache.messages.length,
itemBuilder: (context, index) {
final entry = cache.messages[index];
final widget = entry.senderId == apiClient.userId
? MyMessageBubble(message: entry)
: OtherMessageBubble(message: entry);
if (index == cache.messages.length - 1) {
return Padding(
padding: const EdgeInsets.only(top: 12),
child: widget,
);
}
return widget;
},
);
}, },
), ),
), ),
@ -269,40 +214,43 @@ class _MessagesListState extends State<MessagesList> {
), ),
Padding( Padding(
padding: const EdgeInsets.only(left: 8, right: 4.0), padding: const EdgeInsets.only(left: 8, right: 4.0),
child: IconButton( child: Consumer<MessagingClient>(
splashRadius: 24, builder: (context, mClient, _) {
onPressed: _isSendable && _clientHolder != null ? () async { return IconButton(
setState(() { splashRadius: 24,
_isSendable = false; onPressed: _isSendable ? () async {
}); setState(() {
final message = Message( _isSendable = false;
id: Message.generateId(), });
recipientId: widget.friend.id, final message = Message(
senderId: apiClient.userId, id: Message.generateId(),
type: MessageType.text, recipientId: widget.friend.id,
content: _messageTextController.text, senderId: apiClient.userId,
sendTime: DateTime.now().toUtc(), type: MessageType.text,
content: _messageTextController.text,
sendTime: DateTime.now().toUtc(),
);
try {
mClient.sendMessage(message);
_messageTextController.clear();
setState(() {});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Failed to send message\n$e",
maxLines: null,
),
),
);
setState(() {
_isSendable = true;
});
}
} : null,
iconSize: 28,
icon: const Icon(Icons.send),
); );
try { }
// TODO use provider
//_clientHolder!.messagingClient.sendMessage(message);
_messageTextController.clear();
setState(() {});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Failed to send message\n$e",
maxLines: null,
),
),
);
setState(() {
_isSendable = true;
});
}
} : null,
iconSize: 28,
icon: const Icon(Icons.send),
), ),
) )
], ],