From c9319d32d0d88cf1ed9b49375b13c96db8d34a45 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 6 Dec 2021 12:25:17 -0800 Subject: [PATCH] Much improved message caching --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 1 + .../kotlin/im/cwtch/flwtch/MainActivity.kt | 1 + lib/cwtch/cwtchNotifier.dart | 23 +++++-- lib/model.dart | 17 +++++ lib/models/message.dart | 65 +++++++++++-------- lib/models/messages/filemessage.dart | 7 +- lib/models/messages/invitemessage.dart | 6 +- lib/models/messages/malformedmessage.dart | 3 +- lib/models/messages/quotedmessage.dart | 29 ++++----- lib/models/messages/textmessage.dart | 7 +- lib/views/messageview.dart | 2 + lib/widgets/messagelist.dart | 26 +++++++- lib/widgets/messagerow.dart | 17 +++-- 13 files changed, 137 insertions(+), 67 deletions(-) diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index 7adbdfa9..1ab53ec9 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -184,6 +184,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val profile = (a.get("ProfileOnion") as? String) ?: "" val conversation = a.getInt("conversation").toLong() val indexI = a.getInt("index").toLong() + Log.i("FlwtchWorker", "Cwtch GetMessage " + profile + " " + conversation.toString() + " " + indexI.toString()) return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, conversation, indexI)).build()) } "GetMessageByID" -> { diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index 61447ef5..dec5f220 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -30,6 +30,7 @@ import android.os.Build import android.os.Environment import android.database.Cursor import android.provider.MediaStore +import cwtch.Cwtch class MainActivity: FlutterActivity() { override fun provideSplashScreen(): SplashScreen? = SplashView() diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index c68a89d1..72d68f26 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -130,6 +130,10 @@ class CwtchNotifier { case "NewMessageFromPeer": notificationManager.notify("New Message From Peer!"); var identifier = int.parse(data["ConversationID"]); + var messageID = int.parse(data["Index"]); + var timestamp = DateTime.tryParse(data['TimestampReceived'])!; + var senderHandle = data['RemotePeer']; + var senderImage = data['Picture']; // We might not have received a contact created for this contact yet... // In that case the **next** event we receive will actually update these values... @@ -140,6 +144,7 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; } profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now()); + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, data["Data"], ""); profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; // We only ever see messages from authenticated peers. @@ -156,11 +161,12 @@ class CwtchNotifier { break; case "IndexedAcknowledgement": var conversation = int.parse(data["ConversationID"]); - var message_index = int.parse(data["Index"]); + var messageID = int.parse(data["Index"]); + var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation); // We return -1 for protocol message acks if there is no message - if (message_index == -1) break; - var key = contact!.getMessageKeyOrFail(conversation, message_index, contact.lastMessageTime); + if (messageID == -1) break; + var key = contact!.getMessageKeyOrFail(conversation, messageID, contact.lastMessageTime); if (key == null) break; try { var message = Provider.of(key.currentContext!, listen: false); @@ -181,11 +187,19 @@ class CwtchNotifier { var identifier = int.parse(data["ConversationID"]); if (data["ProfileOnion"] != data["RemotePeer"]) { var idx = int.parse(data["Index"]); + var senderHandle = data['RemotePeer']; + var senderImage = data['Picture']; + var timestampSent = DateTime.tryParse(data['TimestampSent'])!; var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages; // Only bother to do anything if we know about the group and the provided index is greater than our current total... if (currentTotal != null && idx >= currentTotal) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages = idx + 1; + profileCN + .getProfile(data["ProfileOnion"]) + ?.contactList + .getContact(identifier)! + .updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, data["Data"], data["Signature"]); + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; //if not currently open if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) { @@ -194,7 +208,6 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; } - var timestampSent = DateTime.tryParse(data['TimestampSent'])!; // TODO: There are 2 timestamps associated with a new group message - time sent and time received. // Sent refers to the time a profile alleges they sent a message // Received refers to the time we actually saw the message from the server diff --git a/lib/model.dart b/lib/model.dart index a0db9ead..f08113cf 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:cwtch/config.dart'; +import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/messagerow.dart'; import 'package:flutter/cupertino.dart'; import 'package:cwtch/models/profileservers.dart'; @@ -501,6 +502,12 @@ ContactAuthorization stringToContactAuthorization(String authStr) { } } +class MessageCache { + final MessageMetadata metadata; + final String wrapper; + MessageCache(this.metadata, this.wrapper); +} + class ContactInfoState extends ChangeNotifier { final String profileOnion; final int identifier; @@ -515,6 +522,7 @@ class ContactInfoState extends ChangeNotifier { late int _totalMessages = 0; late DateTime _lastMessageTime; late Map> keys; + late List messageCache; int _newMarker = 0; DateTime _newMarkerClearAt = DateTime.now(); @@ -546,6 +554,7 @@ class ContactInfoState extends ChangeNotifier { this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; this._server = server; this._archived = archived; + this.messageCache = List.empty(growable: true); keys = Map>(); } @@ -677,4 +686,12 @@ class ContactInfoState extends ChangeNotifier { GlobalKey ret = keys[index]!; return ret; } + + void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, String data, String signature) { + this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, signature, {}, false, false), data)); + } + + void bumpMessageCache() { + this.messageCache.insert(0, null); + } } diff --git a/lib/models/message.dart b/lib/models/message.dart index 9789bba9..0fab66e9 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -28,11 +28,43 @@ const GroupConversationHandleLength = 32; abstract class Message { MessageMetadata getMetadata(); - Widget getWidget(BuildContext context); + Widget getWidget(BuildContext context, Key key); Widget getPreviewWidget(BuildContext context); } +Message compileOverlay(MessageMetadata metadata, String messageData) { + try { + dynamic message = jsonDecode(messageData); + var content = message['d'] as dynamic; + var overlay = int.parse(message['o'].toString()); + + switch (overlay) { + case TextMessageOverlay: + return TextMessage(metadata, content); + case SuggestContactOverlay: + case InviteGroupOverlay: + return InviteMessage(overlay, metadata, content); + case QuotedMessageOverlay: + return QuotedMessage(metadata, content); + case FileShareOverlay: + return FileMessage(metadata, content); + default: + // Metadata is valid, content is not.. + return MalformedMessage(metadata); + } + } catch (e) { + return MalformedMessage(metadata); + } +} + Future messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) { + var cache = Provider.of(context, listen: false).contactList.getContact(conversationIdentifier)!.messageCache; + if (cache.length > index) { + if (cache[index] != null) { + return Future.value(compileOverlay(cache[index]!.metadata, cache[index]!.wrapper)); + } + } + try { Future rawMessageEnvelopeFuture; @@ -43,7 +75,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co } return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { - var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", "", {}, false, true); + var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, DateTime.now(), "", "", "", {}, false, true); try { dynamic messageWrapper = jsonDecode(rawMessageEnvelope); // There are 2 conditions in which this error condition can be met: @@ -58,7 +90,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { return Future.delayed(Duration(seconds: 2), () { print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); - return messageHandler(context, profileOnion, conversationIdentifier, index, byID: byID).then((value) => value); + return messageHandler(context, profileOnion, conversationIdentifier, -1, byID: byID).then((value) => value); }); } @@ -71,33 +103,16 @@ Future messageHandler(BuildContext context, String profileOnion, int co var ackd = messageWrapper['Acknowledged']; var error = messageWrapper['Error'] != null; var signature = messageWrapper['Signature']; - metadata = MessageMetadata(profileOnion, conversationIdentifier, index, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error); + metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error); - dynamic message = jsonDecode(messageWrapper['Message']); - var content = message['d'] as dynamic; - var overlay = int.parse(message['o'].toString()); - - switch (overlay) { - case TextMessageOverlay: - return TextMessage(metadata, content); - case SuggestContactOverlay: - case InviteGroupOverlay: - return InviteMessage(overlay, metadata, content); - case QuotedMessageOverlay: - return QuotedMessage(metadata, content); - case FileShareOverlay: - return FileMessage(metadata, content); - default: - // Metadata is valid, content is not.. - return MalformedMessage(metadata); - } + return compileOverlay(metadata, messageWrapper['Message']); } catch (e) { EnvironmentConfig.debugLog("an error! " + e.toString()); return MalformedMessage(metadata); } }); } catch (e) { - return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", "", {}, false, true))); + return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, -1, DateTime.now(), "", "", "", {}, false, true))); } } @@ -105,7 +120,6 @@ class MessageMetadata extends ChangeNotifier { // meta-metadata final String profileOnion; final int conversationIdentifier; - final int messageIndex; final int messageID; final DateTime timestamp; @@ -131,6 +145,5 @@ class MessageMetadata extends ChangeNotifier { notifyListeners(); } - MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageIndex, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, - this._error); + MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error); } diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index 3097516c..37562aca 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -17,8 +17,9 @@ class FileMessage extends Message { FileMessage(this.metadata, this.content); @override - Widget getWidget(BuildContext context) { + Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( + key: key, value: this.metadata, builder: (bcontext, child) { dynamic shareObj = jsonDecode(this.content); @@ -34,9 +35,7 @@ class FileMessage extends Message { return MessageRow(MalformedBubble()); } - var lrt = Provider.of(bcontext).lastMessageTime; - return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), - key: Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt)); + return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize)); }); } diff --git a/lib/models/messages/invitemessage.dart b/lib/models/messages/invitemessage.dart index fda0e7fe..3b243a05 100644 --- a/lib/models/messages/invitemessage.dart +++ b/lib/models/messages/invitemessage.dart @@ -17,8 +17,9 @@ class InviteMessage extends Message { InviteMessage(this.overlay, this.metadata, this.content); @override - Widget getWidget(BuildContext context) { + Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( + key: key, value: this.metadata, builder: (bcontext, child) { String inviteTarget; @@ -40,8 +41,7 @@ class InviteMessage extends Message { } } var lrt = Provider.of(bcontext).lastMessageTime; - return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), - key: Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt)); + return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite)); }); } diff --git a/lib/models/messages/malformedmessage.dart b/lib/models/messages/malformedmessage.dart index dc943fe1..ea9896ab 100644 --- a/lib/models/messages/malformedmessage.dart +++ b/lib/models/messages/malformedmessage.dart @@ -9,8 +9,9 @@ class MalformedMessage extends Message { MalformedMessage(this.metadata); @override - Widget getWidget(BuildContext context) { + Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( + key: key, value: this.metadata, builder: (context, child) { return MessageRow(MalformedBubble()); diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index fb835989..9fbbc5cc 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -51,7 +51,7 @@ class QuotedMessage extends Message { dynamic message = jsonDecode(this.content); return Text(message["body"]); } catch (e) { - return MalformedMessage(this.metadata).getWidget(context); + return MalformedMessage(this.metadata).getWidget(context, Key("malformed")); } }); } @@ -62,16 +62,15 @@ class QuotedMessage extends Message { } @override - Widget getWidget(BuildContext context) { + Widget getWidget(BuildContext context, Key key) { try { dynamic message = jsonDecode(this.content); if (message["body"] == null || message["quotedHash"] == null) { - return MalformedMessage(this.metadata).getWidget(context); + return MalformedMessage(this.metadata).getWidget(context, key); } var quotedMessagePotentials = Provider.of(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.conversationIdentifier, message["quotedHash"]); - int messageIndex = metadata.messageIndex; Future quotedMessage = quotedMessagePotentials.then((matchingMessages) { if (matchingMessages == "[]") { return null; @@ -81,9 +80,7 @@ class QuotedMessage extends Message { // message try { var list = (jsonDecode(matchingMessages) as List).map((data) => LocallyIndexedMessage.fromJson(data)).toList(); - LocallyIndexedMessage candidate = list.reversed.firstWhere((element) => messageIndex < element.index, orElse: () { - return list.firstWhere((element) => messageIndex > element.index); - }); + LocallyIndexedMessage candidate = list.reversed.first; return candidate; } catch (e) { // Malformed Message will be returned... @@ -92,20 +89,18 @@ class QuotedMessage extends Message { }); return ChangeNotifierProvider.value( + key: key, value: this.metadata, builder: (bcontext, child) { - var lrt = Provider.of(bcontext).lastMessageTime; - return MessageRow( - QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { - if (localIndex != null) { - return messageHandler(context, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index); - } - return MalformedMessage(this.metadata); - })), - key: Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt)); + return MessageRow(QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { + if (localIndex != null) { + return messageHandler(context, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index); + } + return MalformedMessage(this.metadata); + }))); }); } catch (e) { - return MalformedMessage(this.metadata).getWidget(context); + return MalformedMessage(this.metadata).getWidget(context, key); } } } diff --git a/lib/models/messages/textmessage.dart b/lib/models/messages/textmessage.dart index d1f7fd6b..a94c86f6 100644 --- a/lib/models/messages/textmessage.dart +++ b/lib/models/messages/textmessage.dart @@ -31,14 +31,15 @@ class TextMessage extends Message { } @override - Widget getWidget(BuildContext context) { + Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( + key: key, value: this.metadata, builder: (bcontext, child) { var lrt = Provider.of(bcontext).lastMessageTime; - var key = Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt); + // var key = Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt); - return MessageRow(MessageBubble(this.content), key: key); + return MessageRow(MessageBubble(this.content)); }); } } diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 2b5d225d..7cfd0501 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:crypto/crypto.dart'; +import 'package:cwtch/config.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/messages/quotedmessage.dart'; @@ -213,6 +214,7 @@ class _MessageViewState extends State { ctrlrCompose.clear(); focusNode.requestFocus(); Future.delayed(const Duration(milliseconds: 80), () { + Provider.of(context, listen: false).contactList.getContact(Provider.of(context, listen: false).identifier)?.bumpMessageCache(); Provider.of(context, listen: false).totalMessages++; Provider.of(context, listen: false).newMarker++; // Resort the contact list... diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index f47b03c9..81fb4533 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -79,13 +79,15 @@ class _MessageListState extends State { var contactHandle = Provider.of(outerContext, listen: false).identifier; var messageIndex = index; + // var key = Provider.of(outerContext, listen: false).getMessageKey(contactHandle, Provider.of(outerContext).totalMessages - index, DateTime.now()); return FutureBuilder( + //key: Provider.of(outerContext, listen: false).getMessageKey(contactHandle, Provider.of(outerContext).totalMessages - index, DateTime.now()), future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex), builder: (context, snapshot) { if (snapshot.hasData) { var message = snapshot.data as Message; - // Already includes MessageRow,, - return message.getWidget(context); + var key = Provider.of(outerContext, listen: false).getMessageKey(contactHandle, message.getMetadata().messageID, DateTime.now()); + return message.getWidget(context, key); } else { return MessageLoadingBubble(); } @@ -97,3 +99,23 @@ class _MessageListState extends State { ]))); } } + +class CachedMessage extends Message { + @override + MessageMetadata getMetadata() { + // TODO: implement getMetadata + throw UnimplementedError(); + } + + @override + Widget getPreviewWidget(BuildContext context) { + // TODO: implement getPreviewWidget + throw UnimplementedError(); + } + + @override + Widget getWidget(BuildContext context, Key key) { + // TODO: implement getWidget + throw UnimplementedError(); + } +} diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 963a41ab..311fc3a1 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -34,7 +34,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi @override void initState() { super.initState(); - index = Provider.of(context, listen: false).messageIndex; + index = Provider.of(context, listen: false).messageID; _controller = AnimationController(vsync: this); _controller.addListener(() { setState(() { @@ -75,7 +75,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi } Widget wdgIcons = Visibility( - visible: Provider.of(context).hoveredIndex == Provider.of(context).messageIndex, + visible: Provider.of(context).hoveredIndex == Provider.of(context).messageID, maintainSize: true, maintainAnimation: true, maintainState: true, @@ -169,7 +169,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi // For desktop... onHover: (event) { setState(() { - Provider.of(context, listen: false).hoveredIndex = Provider.of(context, listen: false).messageIndex; + Provider.of(context, listen: false).hoveredIndex = Provider.of(context, listen: false).messageID; }); }, onExit: (event) { @@ -204,7 +204,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi children: widgetRow, ))))); var mark = Provider.of(context).newMarker; - if (mark > 0 && mark == Provider.of(context).messageIndex + 1) { + if (mark > 0 && Provider.of(context).messageCache[mark]?.metadata.messageID == Provider.of(context).messageID) { return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]); } else { return mr; @@ -251,12 +251,17 @@ class MessageRowState extends State with SingleTickerProviderStateMi } void _btnGoto() { - selectConversation(context, Provider.of(context, listen: false).conversationIdentifier); + var id = Provider.of(context, listen: false).contactList.findContact(Provider.of(context, listen: false).senderHandle)?.identifier; + if (id == null) { + // Can't happen + } else { + selectConversation(context, id); + } } void _btnAdd() { var sender = Provider.of(context, listen: false).senderHandle; - if (sender == null || sender == "") { + if (sender == "") { print("sender not yet loaded"); return; }