Much improved message caching
continuous-integration/drone/pr Build is pending Details

This commit is contained in:
Sarah Jamie Lewis 2021-12-06 12:25:17 -08:00
parent c42be6224d
commit c9319d32d0
13 changed files with 137 additions and 67 deletions

View File

@ -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" -> {

View File

@ -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()

View File

@ -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<MessageMetadata>(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

View File

@ -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<String, GlobalKey<MessageRowState>> keys;
late List<MessageCache?> 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<String, GlobalKey<MessageRowState>>();
}
@ -677,4 +686,12 @@ class ContactInfoState extends ChangeNotifier {
GlobalKey<MessageRowState> 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);
}
}

View File

@ -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<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) {
var cache = Provider.of<ProfileInfoState>(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<dynamic> rawMessageEnvelopeFuture;
@ -43,7 +75,7 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, int co
}
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", "", <String, String>{}, false, true);
var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, DateTime.now(), "", "", "", <String, String>{}, false, true);
try {
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
// There are 2 conditions in which this error condition can be met:
@ -58,7 +90,7 @@ Future<Message> 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<Message> 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(), "", "", "", <String, String>{}, false, true)));
return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, -1, DateTime.now(), "", "", "", <String, String>{}, 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);
}

View File

@ -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<ContactInfoState>(bcontext).lastMessageTime;
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize),
key: Provider.of<ContactInfoState>(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt));
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize));
});
}

View File

@ -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<ContactInfoState>(bcontext).lastMessageTime;
return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite),
key: Provider.of<ContactInfoState>(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt));
return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite));
});
}

View File

@ -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());

View File

@ -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<FlwtchState>(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.conversationIdentifier, message["quotedHash"]);
int messageIndex = metadata.messageIndex;
Future<LocallyIndexedMessage?> quotedMessage = quotedMessagePotentials.then((matchingMessages) {
if (matchingMessages == "[]") {
return null;
@ -81,9 +80,7 @@ class QuotedMessage extends Message {
// message
try {
var list = (jsonDecode(matchingMessages) as List<dynamic>).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<ContactInfoState>(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<ContactInfoState>(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);
}
}
}

View File

@ -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<ContactInfoState>(bcontext).lastMessageTime;
var key = Provider.of<ContactInfoState>(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt);
// var key = Provider.of<ContactInfoState>(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt);
return MessageRow(MessageBubble(this.content), key: key);
return MessageRow(MessageBubble(this.content));
});
}
}

View File

@ -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<MessageView> {
ctrlrCompose.clear();
focusNode.requestFocus();
Future.delayed(const Duration(milliseconds: 80), () {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(Provider.of<ContactInfoState>(context, listen: false).identifier)?.bumpMessageCache();
Provider.of<ContactInfoState>(context, listen: false).totalMessages++;
Provider.of<ContactInfoState>(context, listen: false).newMarker++;
// Resort the contact list...

View File

@ -79,13 +79,15 @@ class _MessageListState extends State<MessageList> {
var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).identifier;
var messageIndex = index;
// var key = Provider.of<ContactInfoState>(outerContext, listen: false).getMessageKey(contactHandle, Provider.of<ContactInfoState>(outerContext).totalMessages - index, DateTime.now());
return FutureBuilder(
//key: Provider.of<ContactInfoState>(outerContext, listen: false).getMessageKey(contactHandle, Provider.of<ContactInfoState>(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<ContactInfoState>(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<MessageList> {
])));
}
}
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();
}
}

View File

@ -34,7 +34,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
@override
void initState() {
super.initState();
index = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
index = Provider.of<MessageMetadata>(context, listen: false).messageID;
_controller = AnimationController(vsync: this);
_controller.addListener(() {
setState(() {
@ -75,7 +75,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
}
Widget wdgIcons = Visibility(
visible: Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageIndex,
visible: Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
@ -169,7 +169,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
// For desktop...
onHover: (event) {
setState(() {
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
});
},
onExit: (event) {
@ -204,7 +204,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
children: widgetRow,
)))));
var mark = Provider.of<ContactInfoState>(context).newMarker;
if (mark > 0 && mark == Provider.of<MessageMetadata>(context).messageIndex + 1) {
if (mark > 0 && Provider.of<ContactInfoState>(context).messageCache[mark]?.metadata.messageID == Provider.of<MessageMetadata>(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<MessageRow> with SingleTickerProviderStateMi
}
void _btnGoto() {
selectConversation(context, Provider.of<MessageMetadata>(context, listen: false).conversationIdentifier);
var id = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle)?.identifier;
if (id == null) {
// Can't happen
} else {
selectConversation(context, id);
}
}
void _btnAdd() {
var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
if (sender == null || sender == "") {
if (sender == "") {
print("sender not yet loaded");
return;
}