diff --git a/lib/clients/messaging_client.dart b/lib/clients/messaging_client.dart index 6a9ed1a..7da4cdc 100644 --- a/lib/clients/messaging_client.dart +++ b/lib/clients/messaging_client.dart @@ -52,11 +52,11 @@ class MessagingClient extends ChangeNotifier { final Map _friendsCache = {}; final List _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build() final Map _messageCache = {}; - final Map _messageUpdateListeners = {}; final Map> _unreads = {}; final Logger _logger = Logger("NeosHub"); final Workmanager _workmanager = Workmanager(); final NotificationClient _notificationClient; + Friend? selectedFriend; Timer? _autoRefresh; Timer? _refreshTimeout; int _attempts = 0; @@ -172,16 +172,17 @@ class MessagingClient extends ChangeNotifier { List get cachedFriends => _sortedFriendsCache; - Future getMessageCache(String userId) async { - var cache = _messageCache[userId]; - if (cache == null){ - cache = MessageCache(apiClient: _apiClient, userId: userId); - await cache.loadInitialMessages(); - _messageCache[userId] = cache; - } - return cache; + MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId); + + Future loadUserMessageCache(String userId) async { + final cache = getUserMessageCache(userId) ?? _createUserMessageCache(userId); + await cache.loadMessages(); + _messageCache[userId] = cache; + notifyListeners(); } + MessageCache? getUserMessageCache(String userId) => _messageCache[userId]; + static Future backgroundCheckUnreads(Map? inputData) async { if (inputData == null) return; 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) { final body = jsonDecode((event.toString().replaceAll(eofChar, ""))); final int rawType = body["type"] ?? 0; @@ -301,28 +298,30 @@ class MessagingClient extends ChangeNotifier { case EventTarget.messageSent: final msg = args[0]; 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); - notifyMessageListener(message.recipientId); + notifyListeners(); break; case EventTarget.receiveMessage: final msg = args[0]; final message = Message.fromMap(msg); - final cache = await getMessageCache(message.senderId); + final cache = getUserMessageCache(message.senderId) ?? _createUserMessageCache(message.senderId); cache.addMessage(message); - if (!_messageUpdateListeners.containsKey(message.senderId)) { + if (message.senderId != selectedFriend?.id) { addUnread(message); } - notifyMessageListener(message.senderId); + notifyListeners(); break; case EventTarget.messagesRead: final messageIds = args[0]["ids"] as List; 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) { cache.setMessageState(id, MessageState.read); } - notifyMessageListener(recipientId); + notifyListeners(); break; } } @@ -337,9 +336,9 @@ class MessagingClient extends ChangeNotifier { ], }; _sendData(data); - final cache = await getMessageCache(message.recipientId); + final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId); cache.messages.add(message); - notifyMessageListener(message.recipientId); + notifyListeners(); } void markMessagesRead(MarkReadBatch batch) { @@ -354,3 +353,8 @@ class MessagingClient extends ChangeNotifier { _sendData(data); } } + + +class MessagesProvider extends ChangeNotifier { + +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 86657f4..26c863c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -69,7 +69,7 @@ class _ContactsPlusPlusState extends State { colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark) ), 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) => MessagingClient( apiClient: clientHolder.apiClient, notificationClient: clientHolder.notificationClient), diff --git a/lib/models/message.dart b/lib/models/message.dart index 1695c65..10f5314 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -140,7 +140,7 @@ class MessageCache { return this; //lmao } - Future loadInitialMessages() async { + Future loadMessages() async { final messages = await MessageApi.getUserMessages(_apiClient, userId: _userId); _messages.addAll(messages); _ensureIntegrity(); diff --git a/lib/widgets/friend_list_tile.dart b/lib/widgets/friend_list_tile.dart index 17b19a1..80e37b9 100644 --- a/lib/widgets/friend_list_tile.dart +++ b/lib/widgets/friend_list_tile.dart @@ -1,9 +1,13 @@ 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/message.dart'; import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; import 'package:contacts_plus_plus/widgets/messages_list.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; class FriendListTile extends StatelessWidget { const FriendListTile({required this.friend, this.unreads, this.onTap, super.key}); @@ -24,8 +28,32 @@ class FriendListTile extends StatelessWidget { title: Text(friend.username), subtitle: Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"), onTap: () async { - Navigator.of(context).push(MaterialPageRoute(builder: (context) => MessagesList(friend: friend))); - await onTap?.call(); + onTap?.call(); + final mClient = Provider.of(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.value( + value: mClient, + child: MessagesList(friend: friend), + ), + ), + ); + mClient.selectedFriend = null; }, ); } diff --git a/lib/widgets/friends_list.dart b/lib/widgets/friends_list.dart index 8ff52c4..30d7f15 100644 --- a/lib/widgets/friends_list.dart +++ b/lib/widgets/friends_list.dart @@ -289,19 +289,6 @@ class _FriendsListState extends State { 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(); - }); - }, ); }, ); diff --git a/lib/widgets/messages_list.dart b/lib/widgets/messages_list.dart index 77d771b..2ab80a1 100644 --- a/lib/widgets/messages_list.dart +++ b/lib/widgets/messages_list.dart @@ -1,17 +1,16 @@ -import 'dart:async'; - import 'package:cached_network_image/cached_network_image.dart'; import 'package:contacts_plus_plus/client_holder.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/message.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/generic_avatar.dart'; import 'package:contacts_plus_plus/widgets/message_session_invite.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; class MessagesList extends StatefulWidget { const MessagesList({required this.friend, super.key}); @@ -23,49 +22,18 @@ class MessagesList extends StatefulWidget { } class _MessagesListState extends State { - Future? _messageCacheFuture; final TextEditingController _messageTextController = TextEditingController(); final ScrollController _sessionListScrollController = ScrollController(); final ScrollController _messageScrollController = ScrollController(); - ClientHolder? _clientHolder; bool _isSendable = false; - bool _showSessionListChevron = false; - bool _messageCacheFutureComplete = false; + bool _showSessionListScrollChevron = 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 void dispose() { - // TODO user provider - //_clientHolder?.messagingClient.unregisterMessageListener(widget.friend.id); _messageTextController.dispose(); _sessionListScrollController.dispose(); super.dispose(); @@ -75,26 +43,15 @@ class _MessagesListState extends State { void initState() { super.initState(); _sessionListScrollController.addListener(() { - if (_sessionListScrollController.position.maxScrollExtent > 0 && !_showSessionListChevron) { + if (_sessionListScrollController.position.maxScrollExtent > 0 && !_showSessionListScrollChevron) { setState(() { - _showSessionListChevron = true; + _showSessionListScrollChevron = true; }); } if (_sessionListScrollController.position.atEdge && _sessionListScrollController.position.pixels > 0 - && _showSessionListChevron) { + && _showSessionListScrollChevron) { setState(() { - _showSessionListChevron = 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); + _showSessionListScrollChevron = false; }); } }); @@ -160,68 +117,56 @@ class _MessagesListState extends State { ), ), Expanded( - child: FutureBuilder( - future: _messageCacheFuture, - builder: (context, snapshot) { - if (snapshot.hasData) { - 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 { + child: Consumer( + builder: (context, mClient, _) { + final cache = mClient.getUserMessageCache(widget.friend.id); + if (cache == null) { return Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.start, 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 { ), Padding( padding: const EdgeInsets.only(left: 8, right: 4.0), - child: IconButton( - splashRadius: 24, - onPressed: _isSendable && _clientHolder != null ? () async { - setState(() { - _isSendable = false; - }); - final message = Message( - id: Message.generateId(), - recipientId: widget.friend.id, - senderId: apiClient.userId, - type: MessageType.text, - content: _messageTextController.text, - sendTime: DateTime.now().toUtc(), + child: Consumer( + builder: (context, mClient, _) { + return IconButton( + splashRadius: 24, + onPressed: _isSendable ? () async { + setState(() { + _isSendable = false; + }); + final message = Message( + id: Message.generateId(), + recipientId: widget.friend.id, + senderId: apiClient.userId, + 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), + } ), ) ],